mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-16 17:51:27 +00:00
Merge branch 'release/1.3.0-rc7'
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>ant-kit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -22,6 +22,7 @@ case "$1" in
|
||||
echo Adding shortcut to the menu
|
||||
SECONDARY_LAUNCHERS_INSTALL
|
||||
APP_CDS_CACHE
|
||||
mkdir -pm 644 /usr/share/desktop-directories
|
||||
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
|
||||
FILE_ASSOCIATION_INSTALL
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator Commons</name>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>jacoco-report</artifactId>
|
||||
<name>Cryptomator Code Coverage Report</name>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>keychain</artifactId>
|
||||
<name>System Keychain Access</name>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>launcher</artifactId>
|
||||
<name>Cryptomator Launcher</name>
|
||||
|
||||
@@ -7,17 +7,13 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.EawtApplicationWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -28,9 +24,11 @@ class FileOpenRequestHandler {
|
||||
|
||||
public FileOpenRequestHandler(BlockingQueue<Path> fileOpenRequests) {
|
||||
this.fileOpenRequests = fileOpenRequests;
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
addOsxFileOpenHandler();
|
||||
}
|
||||
EawtApplicationWrapper.getApplication().ifPresent(app -> {
|
||||
app.setOpenFileHandler(files -> {
|
||||
files.stream().map(File::toPath).forEach(fileOpenRequests::add);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void handleLaunchArgs(String[] args) {
|
||||
@@ -55,48 +53,4 @@ class FileOpenRequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event subscription code inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
|
||||
*/
|
||||
private void addOsxFileOpenHandler() {
|
||||
try {
|
||||
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
|
||||
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
|
||||
final Method getApplication = applicationClass.getMethod("getApplication");
|
||||
final Object application = getApplication.invoke(null);
|
||||
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
|
||||
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
|
||||
final OpenFilesEventInvocationHandler openFilesHandler = new OpenFilesEventInvocationHandler();
|
||||
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandler);
|
||||
setOpenFileHandler.invoke(application, openFilesHandlerObject);
|
||||
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||
// Since we're trying to call OS-specific code, we'll just have to hope for the best.
|
||||
LOG.error("Exception adding OS X file open handler", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler class inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
|
||||
*/
|
||||
private class OpenFilesEventInvocationHandler implements InvocationHandler {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if (method.getName().equals("openFiles")) {
|
||||
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
|
||||
final Method getFiles = openFilesEventClass.getMethod("getFiles");
|
||||
Object e = args[0];
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
final List<File> ff = (List<File>) getFiles.invoke(e);
|
||||
ff.stream().map(File::toPath).forEach(fileOpenRequests::add);
|
||||
} catch (RuntimeException ee) {
|
||||
throw ee;
|
||||
} catch (Exception ee) {
|
||||
throw new RuntimeException(ee);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
10
main/pom.xml
10
main/pom.xml
@@ -6,7 +6,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- dependency versions -->
|
||||
<cryptomator.cryptolib.version>1.1.1</cryptomator.cryptolib.version>
|
||||
<cryptomator.cryptofs.version>1.2.2</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptolib.version>1.1.2</cryptomator.cryptolib.version>
|
||||
<cryptomator.cryptofs.version>1.3.0</cryptomator.cryptofs.version>
|
||||
<cryptomator.webdav.version>0.6.1</cryptomator.webdav.version>
|
||||
<cryptomator.jni.version>1.0.2</cryptomator.jni.version>
|
||||
<slf4j.version>1.7.25</slf4j.version>
|
||||
@@ -42,9 +42,9 @@
|
||||
<commons-codec.version>1.10</commons-codec.version>
|
||||
<httpclient.version>4.5.3</httpclient.version>
|
||||
<mockito.version>2.7.21</mockito.version>
|
||||
<dagger.version>2.10</dagger.version>
|
||||
<dagger.version>2.11</dagger.version>
|
||||
<easybind.version>1.0.3</easybind.version>
|
||||
<guava.version>21.0</guava.version>
|
||||
<guava.version>22.0</guava.version>
|
||||
<gson.version>2.8.0</gson.version>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>uber-jar</artifactId>
|
||||
<name>Single über jar with all dependencies</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-rc6</version>
|
||||
<version>1.3.0-rc7</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.model.VaultFactory;
|
||||
import org.cryptomator.ui.model.VaultList;
|
||||
import org.cryptomator.ui.util.DialogBuilderUtil;
|
||||
import org.cryptomator.ui.util.EawtApplicationWrapper;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
@@ -119,6 +120,12 @@ public class MainController implements ViewController {
|
||||
|
||||
EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
|
||||
autoUnlocker.unlockAllSilently();
|
||||
|
||||
EawtApplicationWrapper.getApplication().ifPresent(app -> {
|
||||
app.setPreferencesHandler(() -> {
|
||||
Platform.runLater(this::toggleShowSettings);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -328,10 +335,14 @@ public class MainController implements ViewController {
|
||||
|
||||
@FXML
|
||||
private void didClickShowSettings(ActionEvent e) {
|
||||
toggleShowSettings();
|
||||
}
|
||||
|
||||
private void toggleShowSettings() {
|
||||
if (isShowingSettings.get()) {
|
||||
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
|
||||
showWelcomeView();
|
||||
} else {
|
||||
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
|
||||
showPreferencesView();
|
||||
}
|
||||
vaultList.getSelectionModel().clearSelection();
|
||||
}
|
||||
@@ -375,6 +386,14 @@ public class MainController implements ViewController {
|
||||
// Subcontroller for right panel
|
||||
// ****************************************
|
||||
|
||||
private void showWelcomeView() {
|
||||
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
|
||||
}
|
||||
|
||||
private void showPreferencesView() {
|
||||
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
|
||||
}
|
||||
|
||||
private void showNotFoundView() {
|
||||
activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
||||
import org.cryptomator.frontend.webdav.ServerLifecycleException;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.l10n.Localization;
|
||||
@@ -38,7 +37,6 @@ import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -345,59 +343,42 @@ public class UnlockController implements ViewController {
|
||||
private void didClickUnlockButton(ActionEvent event) {
|
||||
advancedOptions.setDisable(true);
|
||||
progressIndicator.setVisible(true);
|
||||
downloadsPageLink.setVisible(false);
|
||||
CharSequence password = passwordField.getCharacters();
|
||||
asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run();
|
||||
}
|
||||
|
||||
private void unlock(CharSequence password) {
|
||||
try {
|
||||
CharSequence password = passwordField.getCharacters();
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.unlock(password);
|
||||
if (mountAfterUnlock.isSelected()) {
|
||||
vault.mount();
|
||||
if (revealAfterMount.isSelected()) {
|
||||
vault.reveal();
|
||||
}
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
messageText.setText(null);
|
||||
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
|
||||
});
|
||||
if (keychainAccess.isPresent() && savePassword.isSelected()) {
|
||||
keychainAccess.get().storePassphrase(vault.getId(), password);
|
||||
} else {
|
||||
Platform.runLater(passwordField::swipe);
|
||||
}
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Platform.runLater(() -> {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
});
|
||||
} catch (UnsupportedVaultFormatException e) {
|
||||
Platform.runLater(() -> {
|
||||
if (e.isVaultOlderThanSoftware()) {
|
||||
// whitespace after localized text used as separator between text and hyperlink
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
downloadsPageLink.setVisible(true);
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
downloadsPageLink.setVisible(true);
|
||||
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
|
||||
}
|
||||
});
|
||||
} catch (ServerLifecycleException | CommandFailedException e) {
|
||||
}).onSuccess(() -> {
|
||||
messageText.setText(null);
|
||||
downloadsPageLink.setVisible(false);
|
||||
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
|
||||
}).onError(InvalidPassphraseException.class, e -> {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
}).onError(UnsupportedVaultFormatException.class, e -> {
|
||||
if (e.isVaultOlderThanSoftware()) {
|
||||
// whitespace after localized text used as separator between text and hyperlink
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
downloadsPageLink.setVisible(true);
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
downloadsPageLink.setVisible(true);
|
||||
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
|
||||
}
|
||||
}).onError(ServerLifecycleException.class, e -> {
|
||||
LOG.error("Unlock failed for technical reasons.", e);
|
||||
Platform.runLater(() -> {
|
||||
messageText.setText(localization.getString("unlock.errorMessage.mountingFailed"));
|
||||
});
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
advancedOptions.setDisable(false);
|
||||
progressIndicator.setVisible(false);
|
||||
});
|
||||
}
|
||||
messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
|
||||
}).andFinally(() -> {
|
||||
if (!savePassword.isSelected()) {
|
||||
passwordField.swipe();
|
||||
}
|
||||
advancedOptions.setDisable(false);
|
||||
progressIndicator.setVisible(false);
|
||||
}).run();
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
@@ -10,10 +10,13 @@ package org.cryptomator.ui.controllers;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.cryptomator.frontend.webdav.ServerLifecycleException;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
|
||||
import org.cryptomator.ui.l10n.Localization;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.AsyncTaskService;
|
||||
@@ -22,9 +25,12 @@ import org.fxmisc.easybind.EasyBind;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.util.concurrent.Runnables;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -58,6 +64,7 @@ public class UnlockedController implements ViewController {
|
||||
private final Localization localization;
|
||||
private final AsyncTaskService asyncTaskService;
|
||||
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
|
||||
private final BooleanExpression vaultMounted = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(Vault::mountedProperty).orElse(false));
|
||||
private Optional<LockListener> listener = Optional.empty();
|
||||
private Timeline ioAnimation;
|
||||
|
||||
@@ -76,6 +83,12 @@ public class UnlockedController implements ViewController {
|
||||
@FXML
|
||||
private ContextMenu moreOptionsMenu;
|
||||
|
||||
@FXML
|
||||
private MenuItem mountVaultMenuItem;
|
||||
|
||||
@FXML
|
||||
private MenuItem unmountVaultMenuItem;
|
||||
|
||||
@FXML
|
||||
private MenuItem revealVaultMenuItem;
|
||||
|
||||
@@ -90,7 +103,9 @@ public class UnlockedController implements ViewController {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
revealVaultMenuItem.disableProperty().bind(EasyBind.map(vault, vault -> vault != null && !vault.isMounted()));
|
||||
mountVaultMenuItem.disableProperty().bind(vaultMounted);
|
||||
unmountVaultMenuItem.disableProperty().bind(vaultMounted.not());
|
||||
revealVaultMenuItem.disableProperty().bind(vaultMounted.not());
|
||||
|
||||
EasyBind.subscribe(vault, this::vaultChanged);
|
||||
EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
|
||||
@@ -106,9 +121,8 @@ public class UnlockedController implements ViewController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newVault.getVaultSettings().mountAfterUnlock().get() && !newVault.isMounted()) {
|
||||
// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
|
||||
messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
|
||||
if (newVault.getVaultSettings().mountAfterUnlock().get()) {
|
||||
mountVault(newVault);
|
||||
}
|
||||
|
||||
// (re)start throughput statistics:
|
||||
@@ -118,61 +132,16 @@ public class UnlockedController implements ViewController {
|
||||
|
||||
@FXML
|
||||
private void didClickLockVault(ActionEvent event) {
|
||||
regularLockVault();
|
||||
regularUnmountVault(this::lockVault);
|
||||
}
|
||||
|
||||
private void regularLockVault() {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.get().unmount();
|
||||
private void lockVault() {
|
||||
try {
|
||||
vault.get().lock();
|
||||
}).onSuccess(() -> {
|
||||
listener.ifPresent(listener -> listener.didLock(this));
|
||||
LOG.trace("Regular lock succeeded");
|
||||
}).onError(Exception.class, e -> {
|
||||
onRegularLockVaultFailed(e);
|
||||
}).run();
|
||||
}
|
||||
|
||||
private void forcedLockVault() {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.get().unmountForced();
|
||||
vault.get().lock();
|
||||
}).onSuccess(() -> {
|
||||
listener.ifPresent(listener -> listener.didLock(this));
|
||||
LOG.trace("Forced lock succeeded");
|
||||
}).onError(Exception.class, e -> {
|
||||
onForcedLockVaultFailed(e);
|
||||
}).run();
|
||||
}
|
||||
|
||||
private void onRegularLockVaultFailed(Exception e) {
|
||||
if (vault.get().supportsForcedUnmount()) {
|
||||
LOG.trace("Regular unmount failed", e);
|
||||
Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
|
||||
format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
|
||||
localization.getString("unlocked.lock.force.confirmation.header"), //
|
||||
localization.getString("unlocked.lock.force.confirmation.content"), //
|
||||
ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> choice = confirmDialog.showAndWait();
|
||||
if (ButtonType.YES.equals(choice.get())) {
|
||||
forcedLockVault();
|
||||
} else {
|
||||
LOG.trace("Unmount cancelled", e);
|
||||
}
|
||||
} else {
|
||||
LOG.error("Regular unmount failed", e);
|
||||
showUnmountFailedMessage();
|
||||
} catch (ServerLifecycleException | IOException e) {
|
||||
LOG.error("Lock failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onForcedLockVaultFailed(Exception e) {
|
||||
LOG.error("Forced unmount failed", e);
|
||||
showUnmountFailedMessage();
|
||||
}
|
||||
|
||||
private void showUnmountFailedMessage() {
|
||||
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
|
||||
listener.ifPresent(listener -> listener.didLock(this));
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -186,11 +155,88 @@ public class UnlockedController implements ViewController {
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickRevealVault(ActionEvent event) {
|
||||
public void didClickMountVault(ActionEvent event) {
|
||||
mountVault(vault.get());
|
||||
}
|
||||
|
||||
private void mountVault(Vault vault) {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.get().reveal();
|
||||
}).onError(RuntimeException.class, () -> {
|
||||
// TODO overheadhunter catch more specific exception type thrown by reveal()
|
||||
vault.mount();
|
||||
}).onSuccess(() -> {
|
||||
LOG.trace("Mount succeeded.");
|
||||
messageLabel.setText(null);
|
||||
if (vault.getVaultSettings().revealAfterMount().get()) {
|
||||
revealVault(vault);
|
||||
}
|
||||
}).onError(CommandFailedException.class, e -> {
|
||||
LOG.error("Mount failed.", e);
|
||||
// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
|
||||
messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
|
||||
}).run();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickUnmountVault(ActionEvent event) {
|
||||
regularUnmountVault(Runnables.doNothing());
|
||||
}
|
||||
|
||||
private void regularUnmountVault(Runnable onSuccess) {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.get().unmount();
|
||||
}).onSuccess(() -> {
|
||||
LOG.trace("Regular unmount succeeded.");
|
||||
onSuccess.run();
|
||||
}).onError(Exception.class, e -> {
|
||||
onRegularUnmountVaultFailed(e, onSuccess);
|
||||
}).run();
|
||||
}
|
||||
|
||||
private void forcedUnmountVault(Runnable onSuccess) {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.get().unmountForced();
|
||||
}).onSuccess(() -> {
|
||||
LOG.trace("Forced unmount succeeded.");
|
||||
onSuccess.run();
|
||||
}).onError(Exception.class, e -> {
|
||||
LOG.error("Forced unmount failed.", e);
|
||||
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
|
||||
}).run();
|
||||
}
|
||||
|
||||
private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) {
|
||||
if (vault.get().supportsForcedUnmount()) {
|
||||
LOG.trace("Regular unmount failed.", e);
|
||||
Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
|
||||
format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
|
||||
localization.getString("unlocked.lock.force.confirmation.header"), //
|
||||
localization.getString("unlocked.lock.force.confirmation.content"), //
|
||||
ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> choice = confirmDialog.showAndWait();
|
||||
if (ButtonType.YES.equals(choice.get())) {
|
||||
forcedUnmountVault(onSuccess);
|
||||
} else {
|
||||
LOG.trace("Unmount cancelled.", e);
|
||||
}
|
||||
} else {
|
||||
LOG.error("Regular unmount failed.", e);
|
||||
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickRevealVault(ActionEvent event) {
|
||||
revealVault(vault.get());
|
||||
}
|
||||
|
||||
private void revealVault(Vault vault) {
|
||||
asyncTaskService.asyncTaskOf(() -> {
|
||||
vault.reveal();
|
||||
}).onSuccess(() -> {
|
||||
LOG.trace("Reveal succeeded.");
|
||||
messageLabel.setText(null);
|
||||
}).onError(CommandFailedException.class, e -> {
|
||||
LOG.error("Reveal failed.", e);
|
||||
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
|
||||
}).run();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.nio.file.FileSystem;
|
||||
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.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
@@ -30,9 +31,11 @@ import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.frontend.webdav.ServerLifecycleException;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.cryptomator.frontend.webdav.mount.MountParams;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
|
||||
@@ -78,12 +81,13 @@ public class Vault {
|
||||
// ********************************************************************************/
|
||||
|
||||
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
|
||||
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class);
|
||||
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
|
||||
}
|
||||
|
||||
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
|
||||
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withFlags(EnumSet.noneOf(FileSystemFlags.class)) // TODO: use withFlags() with CryptoFS 1.3.1
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
@@ -98,7 +102,7 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
if (!isValidVaultDirectory()) {
|
||||
createCryptoFileSystem(passphrase).close(); // implicitly creates a non-existing vault
|
||||
CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
|
||||
} else {
|
||||
throw new FileAlreadyExistsException(getPath().toString());
|
||||
}
|
||||
@@ -108,7 +112,7 @@ public class Vault {
|
||||
CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) {
|
||||
public synchronized void unlock(CharSequence passphrase) throws ServerLifecycleException {
|
||||
try {
|
||||
FileSystem fs = getCryptoFileSystem(passphrase);
|
||||
if (!server.isRunning()) {
|
||||
@@ -161,7 +165,7 @@ public class Vault {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
}
|
||||
|
||||
public synchronized void lock() throws Exception {
|
||||
public synchronized void lock() throws ServerLifecycleException, IOException {
|
||||
if (servlet != null) {
|
||||
servlet.stop();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ public class AsyncTaskService {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async task
|
||||
*
|
||||
* @param task Tasks to be invoked in a background thread.
|
||||
* @return The async task
|
||||
*/
|
||||
public AsyncTaskWithoutSuccessHandler<Void> asyncTaskOf(RunnableThrowingException<?> task) {
|
||||
return new AsyncTaskImpl<>(() -> {
|
||||
task.run();
|
||||
@@ -39,6 +45,12 @@ public class AsyncTaskService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async task
|
||||
*
|
||||
* @param task Tasks to be invoked in a background thread.
|
||||
* @return The async task
|
||||
*/
|
||||
public <ResultType> AsyncTaskWithoutSuccessHandler<ResultType> asyncTaskOf(SupplierThrowingException<ResultType, ?> task) {
|
||||
return new AsyncTaskImpl<>(task);
|
||||
}
|
||||
@@ -153,27 +165,55 @@ public class AsyncTaskService {
|
||||
|
||||
public interface AsyncTaskWithoutSuccessHandler<ResultType> extends AsyncTaskWithoutErrorHandler {
|
||||
|
||||
/**
|
||||
* @param handler Tasks to be invoked on the JavaFX application thread.
|
||||
* @return The async task
|
||||
*/
|
||||
AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler);
|
||||
|
||||
/**
|
||||
* @param handler Tasks to be invoked on the JavaFX application thread.
|
||||
* @return The async task
|
||||
*/
|
||||
AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler {
|
||||
|
||||
/**
|
||||
* @param type Exception type to catch
|
||||
* @param handler Tasks to be invoked on the JavaFX application thread.
|
||||
* @return The async task
|
||||
*/
|
||||
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
|
||||
|
||||
/**
|
||||
* @param type Exception type to catch
|
||||
* @param handler Tasks to be invoked on the JavaFX application thread.
|
||||
* @return The async task
|
||||
*/
|
||||
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTaskWithoutFinallyHandler extends AsyncTask {
|
||||
|
||||
/**
|
||||
* @param handler Tasks to be invoked on the JavaFX application thread.
|
||||
* @return The async task
|
||||
*/
|
||||
AsyncTask andFinally(RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTask extends Runnable {
|
||||
|
||||
/**
|
||||
* Starts the async task.
|
||||
*/
|
||||
@Override
|
||||
void run();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*******************************************************************************
|
||||
* 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.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.SupplierThrowingException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Reflection-based wrapper for com.apple.eawt.Application.
|
||||
*/
|
||||
public class EawtApplicationWrapper {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EawtApplicationWrapper.class);
|
||||
|
||||
private final Class<?> applicationClass;
|
||||
private final Object application;
|
||||
|
||||
private EawtApplicationWrapper() throws ReflectiveOperationException {
|
||||
this.applicationClass = Class.forName("com.apple.eawt.Application");
|
||||
this.application = applicationClass.getMethod("getApplication").invoke(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A wrapper for com.apple.ewat.Application if the current OS is macOS and the class is available in this JVM.
|
||||
*/
|
||||
public static Optional<EawtApplicationWrapper> getApplication() {
|
||||
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(new EawtApplicationWrapper());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void setOpenFileHandler(InvocationHandler handler) throws ReflectiveOperationException {
|
||||
Class<?> handlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
|
||||
Method setter = applicationClass.getMethod("setOpenFileHandler", handlerClass);
|
||||
Object proxy = Proxy.newProxyInstance(applicationClass.getClassLoader(), new Class<?>[] {handlerClass}, handler);
|
||||
setter.invoke(application, proxy);
|
||||
}
|
||||
|
||||
public void setOpenFileHandler(Consumer<List<File>> handler) {
|
||||
try {
|
||||
Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
|
||||
Method getFiles = openFilesEventClass.getMethod("getFiles");
|
||||
setOpenFileHandler(methodSpecificInvocationHandler("openFiles", args -> {
|
||||
Object openFilesEvent = args[0];
|
||||
assert openFilesEventClass.isInstance(openFilesEvent);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<File> files = (List<File>) uncheckedReflectiveOperation(() -> getFiles.invoke(openFilesEvent));
|
||||
handler.accept(files);
|
||||
return null;
|
||||
}));
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.error("Exception setting openFileHandler.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPreferencesHandler(InvocationHandler handler) throws ReflectiveOperationException {
|
||||
Class<?> handlerClass = Class.forName("com.apple.eawt.PreferencesHandler");
|
||||
Method setter = applicationClass.getMethod("setPreferencesHandler", handlerClass);
|
||||
Object proxy = Proxy.newProxyInstance(applicationClass.getClassLoader(), new Class<?>[] {handlerClass}, handler);
|
||||
setter.invoke(application, proxy);
|
||||
}
|
||||
|
||||
public void setPreferencesHandler(Runnable handler) {
|
||||
try {
|
||||
setPreferencesHandler(methodSpecificInvocationHandler("handlePreferences", args -> {
|
||||
handler.run();
|
||||
return null;
|
||||
}));
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.error("Exception setting preferencesHandler.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static InvocationHandler methodSpecificInvocationHandler(String methodName, Function<Object[], Object> handler) {
|
||||
return new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if (method.getName().equals(methodName)) {
|
||||
return handler.apply(args);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unexpected invocation " + method.getName() + ", expected " + methodName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps {@link ReflectiveOperationException}s as {@link UncheckedReflectiveOperationException}.
|
||||
*
|
||||
* @param operation Invokation throwing an ReflectiveOperationException
|
||||
* @return Result returned by <code>operation</code>
|
||||
* @throws UncheckedReflectiveOperationException in case <code>operation</code> throws an ReflectiveOperationException.
|
||||
*/
|
||||
private static <T> T uncheckedReflectiveOperation(SupplierThrowingException<T, ReflectiveOperationException> operation) throws UncheckedReflectiveOperationException {
|
||||
try {
|
||||
return operation.get();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new UncheckedReflectiveOperationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UncheckedReflectiveOperationException extends RuntimeException {
|
||||
public UncheckedReflectiveOperationException(ReflectiveOperationException cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,12 @@
|
||||
<fx:define>
|
||||
<ContextMenu fx:id="moreOptionsMenu">
|
||||
<items>
|
||||
<MenuItem fx:id="mountVaultMenuItem" text="%unlocked.moreOptions.mount" onAction="#didClickMountVault">
|
||||
<graphic><Label text="" styleClass="ionicons"/></graphic>
|
||||
</MenuItem>
|
||||
<MenuItem fx:id="unmountVaultMenuItem" text="%unlocked.moreOptions.unmount" onAction="#didClickUnmountVault">
|
||||
<graphic><Label text="" styleClass="ionicons"/></graphic>
|
||||
</MenuItem>
|
||||
<MenuItem fx:id="revealVaultMenuItem" text="%unlocked.moreOptions.reveal" onAction="#didClickRevealVault">
|
||||
<graphic><Label text="" styleClass="ionicons"/></graphic>
|
||||
</MenuItem>
|
||||
|
||||
@@ -72,7 +72,7 @@ unlock.savePassword.delete.confirmation.header=Do you really want to delete the
|
||||
unlock.savePassword.delete.confirmation.content=The saved password of this vault will be immediately deleted from your system keychain. If you'd like to save your password again, you'd have to unlock your vault with the "Save Password" option enabled.
|
||||
unlock.choicebox.winDriveLetter.auto=Assign automatically
|
||||
unlock.errorMessage.wrongPassword=Wrong password
|
||||
unlock.errorMessage.mountingFailed=Mounting failed. See log file for details.
|
||||
unlock.errorMessage.unlockFailed=Unlock failed. See log file for details.
|
||||
unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
|
||||
unlock.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
|
||||
unlock.errorMessage.unauthenticVersionMac=Could not authenticate version MAC.
|
||||
@@ -89,6 +89,8 @@ changePassword.errorMessage.decryptionFailed=Decryption failed
|
||||
|
||||
# unlocked.fxml
|
||||
unlocked.button.lock=Lock Vault
|
||||
unlocked.moreOptions.mount=Mount Drive
|
||||
unlocked.moreOptions.unmount=Eject Drive
|
||||
unlocked.moreOptions.reveal=Reveal Drive
|
||||
unlocked.moreOptions.copyUrl=Copy WebDAV URL
|
||||
unlocked.label.mountFailed=Connecting drive failed
|
||||
|
||||
Reference in New Issue
Block a user