From 695dcd5de75168a93ec0b2770bfc061d83c40a00 Mon Sep 17 00:00:00 2001 From: Markus Kreusch Date: Fri, 28 Apr 2017 12:53:53 +0200 Subject: [PATCH 01/23] Using orange color from iOS for unlocked vault icons --- .../cryptomator/ui/controls/DirectoryListCell.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java b/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java index 74f98177a..7676d6dc9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java @@ -20,9 +20,12 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; public class DirectoryListCell extends DraggableListCell { + private static final Color UNLOCKED_ICON_COLOR = new Color(0.901, 0.494, 0.133, 1.0); private final Label statusText = new Label(); private final Label nameText = new Label(); private final Label pathText = new Label(); @@ -45,7 +48,7 @@ public class DirectoryListCell extends DraggableListCell { MonadicBinding optionalItemIsUnlocked = EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty); statusText.textProperty().bind(optionalItemIsUnlocked.map(this::getStatusIconText)); - statusText.textFillProperty().bind(this.textFillProperty()); + statusText.textFillProperty().bind(EasyBind.combine(optionalItemIsUnlocked, textFillProperty(), this::getStatusIconColor)); statusText.setMinSize(16.0, 16.0); statusText.setAlignment(Pos.CENTER); statusText.getStyleClass().add("fontawesome"); @@ -67,6 +70,14 @@ public class DirectoryListCell extends DraggableListCell { } } + private Paint getStatusIconColor(Boolean unlockedOrNull, Paint lockedValue) { + if (Boolean.TRUE.equals(unlockedOrNull)) { + return UNLOCKED_ICON_COLOR; + } else { + return lockedValue; + } + } + public void setVaultContextMenu(ContextMenu contextMenu) { this.vaultContextMenu = contextMenu; } From d48247b7c6157d8c4f55a717f7ef1cca3b2fe98e Mon Sep 17 00:00:00 2001 From: Markus Kreusch Date: Fri, 28 Apr 2017 13:23:51 +0200 Subject: [PATCH 02/23] #386: Allow forced locking after failed locking on Windows --- main/pom.xml | 2 +- .../ui/controllers/UnlockedController.java | 60 ++++++++++++++++++- .../java/org/cryptomator/ui/model/Vault.java | 15 ++++- .../ui/util/DialogBuilderUtil.java | 8 ++- .../ui/src/main/resources/localization/en.txt | 3 + 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/main/pom.xml b/main/pom.xml index d11d564b0..3f8b97d42 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -29,7 +29,7 @@ 1.1.1 1.2.2 - 0.5.1 + 0.6.0-SNAPSHOT 1.0.1 2.8.1 1.7.25 diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index c0efa6fdf..4b04fd1ed 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -8,6 +8,8 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; +import static java.lang.String.format; + import java.util.Optional; import javax.inject.Inject; @@ -15,7 +17,10 @@ import javax.inject.Inject; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; +import org.cryptomator.ui.util.DialogBuilderUtil; import org.fxmisc.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javafx.animation.Animation; import javafx.animation.KeyFrame; @@ -31,6 +36,8 @@ import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart.Data; import javafx.scene.chart.XYChart.Series; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -43,6 +50,8 @@ import javafx.util.Duration; public class UnlockedController implements ViewController { + private static final Logger LOG = LoggerFactory.getLogger(UnlockedController.class); + private static final int IO_SAMPLING_STEPS = 100; private static final double IO_SAMPLING_INTERVAL = 0.25; @@ -109,16 +118,63 @@ public class UnlockedController implements ViewController { @FXML private void didClickLockVault(ActionEvent event) { + regularLockVault(); + } + + private void regularLockVault() { asyncTaskService.asyncTaskOf(() -> { vault.get().unmount(); vault.get().lock(); }).onSuccess(() -> { listener.ifPresent(listener -> listener.didLock(this)); - }).onError(Exception.class, () -> { - messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + 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 choice = confirmDialog.showAndWait(); + if (ButtonType.YES.equals(choice.get())) { + forcedLockVault(); + } else { + LOG.trace("Unmount cancelled", e); + } + } else { + LOG.error("Regular unmount failed", e); + showUnmountFailedMessage(); + } + } + + private void onForcedLockVaultFailed(Exception e) { + LOG.error("Forced unmount failed", e); + showUnmountFailedMessage(); + } + + private void showUnmountFailedMessage() { + messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + } + @FXML private void didClickMoreOptions(ActionEvent event) { if (moreOptionsMenu.isShowing()) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 4e84a5f39..d972c3ab6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -23,6 +23,7 @@ import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.ConsumerThrowingException; import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; @@ -146,14 +147,26 @@ public class Vault { } public synchronized void unmount() throws Exception { + unmount(mount -> mount.unmount()); + } + + public synchronized void unmountForced() throws Exception { + unmount(mount -> mount.forced().get().unmount()); + } + + private synchronized void unmount(ConsumerThrowingException command) throws CommandFailedException { if (mount != null) { - mount.unmount(); + command.accept(mount); } Platform.runLater(() -> { mounted.set(false); }); } + public boolean supportsForcedUnmount() { + return mount != null && mount.forced().isPresent(); + } + public synchronized void lock() throws Exception { if (servlet != null) { servlet.stop(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java index e9f1b99fe..55f6539a7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java @@ -34,11 +34,15 @@ public class DialogBuilderUtil { return buildDialog(title, header, content, Alert.AlertType.CONFIRMATION, defaultButton); } - private static Alert buildDialog(String title, String header, String content, Alert.AlertType type, ButtonType defaultButton) { + public static Alert buildYesNoDialog(String title, String header, String content, ButtonType defaultButton) { + return buildDialog(title, header, content, Alert.AlertType.CONFIRMATION, defaultButton, ButtonType.YES, ButtonType.NO); + } + + private static Alert buildDialog(String title, String header, String content, Alert.AlertType type, ButtonType defaultButton, ButtonType... buttons) { Text contentText = new Text(content); contentText.setWrappingWidth(360.0); - Alert alert = new Alert(type); + Alert alert = new Alert(type, content, buttons); alert.setTitle(title); alert.setHeaderText(header); alert.getDialogPane().setContent(contentText); diff --git a/main/ui/src/main/resources/localization/en.txt b/main/ui/src/main/resources/localization/en.txt index 16468bb52..393faa982 100644 --- a/main/ui/src/main/resources/localization/en.txt +++ b/main/ui/src/main/resources/localization/en.txt @@ -96,6 +96,9 @@ unlocked.label.unmountFailed=Ejecting drive failed unlocked.label.statsEncrypted=encrypted unlocked.label.statsDecrypted=decrypted unlocked.ioGraph.yAxis.label=Throughput (MiB/s) +unlocked.lock.force.confirmation.title=Locking of %1$s failed +unlocked.lock.force.confirmation.header=Do you want to force locking? +unlocked.lock.force.confirmation.content=This may be because other programs are still accessing files in the vault or because some other problem occurred.\n\nPrograms still accessing the files may not work correctly and data not already written by those programs may be lost. # settings.fxml settings.version.label=Version %s From e63cbf94d015f3f8391028bad074e057a5d2cb0e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 28 Apr 2017 23:54:23 +0200 Subject: [PATCH 03/23] Logging IOExceptions in main method. --- .../main/java/org/cryptomator/launcher/Cryptomator.java | 4 +++- .../cryptomator/launcher/InterProcessCommunicator.java | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java index 17b1096e5..5daa2cc91 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -17,7 +17,7 @@ public class Cryptomator { private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class); static final BlockingQueue FILE_OPEN_REQUESTS = new ArrayBlockingQueue<>(10); - public static void main(String[] args) throws IOException { + public static void main(String[] args) { LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); FileOpenRequestHandler fileOpenRequestHandler = new FileOpenRequestHandler(FILE_OPEN_REQUESTS); @@ -30,6 +30,8 @@ public class Cryptomator { communicator.handleLaunchArgs(args); LOG.info("Found running application instance. Shutting down."); } + } catch (IOException e) { + LOG.error("Failed to initiate inter-process communication.", e); } System.exit(0); // end remaining non-daemon threads. } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java index a021067c0..e0b3af685 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java @@ -12,7 +12,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.rmi.ConnectException; -import java.rmi.NoSuchObjectException; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; @@ -113,7 +112,7 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt } @Override - public void close() throws IOException { + public void close() { // no-op } @@ -150,14 +149,14 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt } @Override - public void close() throws IOException { + public void close() { try { registry.unbind(RMI_NAME); UnicastRemoteObject.unexportObject(remote, true); socket.close(); LOG.debug("Server shut down."); - } catch (NotBoundException | NoSuchObjectException e) { - // ignore + } catch (NotBoundException | IOException e) { + LOG.warn("Failed to close IPC Server.", e); } } From c0460567368f965937c1baf3b9a2ff432d187efd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 29 Apr 2017 00:09:15 +0200 Subject: [PATCH 04/23] Updated JNI dependency [ci skip] --- main/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/pom.xml b/main/pom.xml index 3f8b97d42..125c1b513 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -30,7 +30,7 @@ 1.1.1 1.2.2 0.6.0-SNAPSHOT - 1.0.1 + 1.0.2 2.8.1 1.7.25 4.12 From f62c0b4ca830d0b52f3c87dad1ef96001f804b51 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 29 Apr 2017 10:12:39 +0200 Subject: [PATCH 05/23] Create non-existing parent directories of IPC port file. --- .../java/org/cryptomator/launcher/InterProcessCommunicator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java index e0b3af685..3c77aadf0 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java @@ -221,6 +221,7 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); buf.putInt(port); buf.flip(); + Files.createDirectories(path.getParent()); try (WritableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { if (ch.write(buf) != Integer.BYTES) { throw new IOException("Did not write expected number of bytes."); From d03446beef52cbf143edfc1deb86e6d4b1dccc96 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 29 Apr 2017 10:31:22 +0200 Subject: [PATCH 06/23] adjusted unit tests --- .../cryptomator/launcher/InterProcessCommunicatorTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java index c258efb1b..c8f2fe534 100644 --- a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java +++ b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java @@ -17,6 +17,7 @@ import org.mockito.Mockito; public class InterProcessCommunicatorTest { Path portFilePath = Mockito.mock(Path.class); + Path portFileParentPath = Mockito.mock(Path.class); FileSystem fs = Mockito.mock(FileSystem.class); FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); SeekableByteChannel portFileChannel = Mockito.mock(SeekableByteChannel.class); @@ -25,6 +26,8 @@ public class InterProcessCommunicatorTest { @Before public void setup() throws IOException { Mockito.when(portFilePath.getFileSystem()).thenReturn(fs); + Mockito.when(portFilePath.getParent()).thenReturn(portFileParentPath); + Mockito.when(portFileParentPath.getFileSystem()).thenReturn(fs); Mockito.when(fs.provider()).thenReturn(provider); Mockito.when(provider.newByteChannel(Mockito.eq(portFilePath), Mockito.any(), Mockito.any())).thenReturn(portFileChannel); Mockito.when(portFileChannel.read(Mockito.any())).then(invocation -> { @@ -45,6 +48,7 @@ public class InterProcessCommunicatorTest { InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class); try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) { Assert.assertTrue(result.isServer()); + Mockito.verify(provider).createDirectory(portFileParentPath); Mockito.verifyZeroInteractions(protocol); result.handleLaunchArgs(new String[] {"foo"}); } @@ -57,6 +61,7 @@ public class InterProcessCommunicatorTest { InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class); try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) { Assert.assertTrue(result.isServer()); + Mockito.verify(provider).createDirectory(portFileParentPath); Mockito.verifyZeroInteractions(protocol); } } @@ -67,10 +72,12 @@ public class InterProcessCommunicatorTest { InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class); try (InterProcessCommunicator result1 = InterProcessCommunicator.start(portFilePath, protocol)) { Assert.assertTrue(result1.isServer()); + Mockito.verify(provider, Mockito.times(1)).createDirectory(portFileParentPath); Mockito.verifyZeroInteractions(protocol); try (InterProcessCommunicator result2 = InterProcessCommunicator.start(portFilePath, null)) { Assert.assertFalse(result2.isServer()); + Mockito.verify(provider, Mockito.times(1)).createDirectory(portFileParentPath); Assert.assertNotSame(result1, result2); result2.handleLaunchArgs(new String[] {"foo"}); From 8cada6d0a231e67deca0edcab6312bea5e50f1b6 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 30 Apr 2017 00:21:29 +0200 Subject: [PATCH 07/23] Guava convenience functions instead of reinventing the wheel --- .../java/org/cryptomator/common/LazyInitializer.java | 12 +++++------- .../launcher/InterProcessCommunicator.java | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java index 4d166bbb3..0d14ce339 100644 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java @@ -4,6 +4,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import com.google.common.base.Throwables; + public final class LazyInitializer { private LazyInitializer() { @@ -41,11 +43,8 @@ public final class LazyInitializer { try { return reference.updateAndGet(invokeFactoryIfNull(factory)); } catch (InitializationException e) { - if (exceptionType.isInstance(e.getCause())) { - throw exceptionType.cast(e.getCause()); - } else { - throw e; - } + Throwables.throwIfInstanceOf(e.getCause(), exceptionType); + throw e; } } } @@ -55,9 +54,8 @@ public final class LazyInitializer { if (currentValue == null) { try { return factory.get(); - } catch (RuntimeException e) { - throw e; // don't catch unchecked exceptions } catch (Exception e) { + Throwables.throwIfUnchecked(e); // don't catch unchecked exceptions throw new InitializationException(e); } } else { diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java index 3c77aadf0..ed72e17be 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java @@ -26,6 +26,8 @@ import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.io.MoreFiles; + /** * First running application on a machine opens a server socket. Further processes will connect as clients. */ @@ -221,7 +223,7 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); buf.putInt(port); buf.flip(); - Files.createDirectories(path.getParent()); + MoreFiles.createParentDirectories(path); try (WritableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { if (ch.write(buf) != Integer.BYTES) { throw new IOException("Did not write expected number of bytes."); From c29d7fb6a213a6ca0390bd59c5089d79cc584810 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 30 Apr 2017 00:59:07 +0200 Subject: [PATCH 08/23] fixes unit test --- .../cryptomator/launcher/InterProcessCommunicatorTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java index c8f2fe534..63d8894f7 100644 --- a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java +++ b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java @@ -6,6 +6,7 @@ import java.nio.channels.SeekableByteChannel; import java.nio.file.FileSystem; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; import java.util.concurrent.atomic.AtomicInteger; @@ -18,6 +19,7 @@ public class InterProcessCommunicatorTest { Path portFilePath = Mockito.mock(Path.class); Path portFileParentPath = Mockito.mock(Path.class); + BasicFileAttributes portFileParentPathAttrs = Mockito.mock(BasicFileAttributes.class); FileSystem fs = Mockito.mock(FileSystem.class); FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); SeekableByteChannel portFileChannel = Mockito.mock(SeekableByteChannel.class); @@ -26,9 +28,13 @@ public class InterProcessCommunicatorTest { @Before public void setup() throws IOException { Mockito.when(portFilePath.getFileSystem()).thenReturn(fs); + Mockito.when(portFilePath.toAbsolutePath()).thenReturn(portFilePath); + Mockito.when(portFilePath.normalize()).thenReturn(portFilePath); Mockito.when(portFilePath.getParent()).thenReturn(portFileParentPath); Mockito.when(portFileParentPath.getFileSystem()).thenReturn(fs); Mockito.when(fs.provider()).thenReturn(provider); + Mockito.when(provider.readAttributes(portFileParentPath, BasicFileAttributes.class)).thenReturn(portFileParentPathAttrs); + Mockito.when(portFileParentPathAttrs.isDirectory()).thenReturn(false, true); // Guava's MoreFiles will check if dir exists before attempting to create them. Mockito.when(provider.newByteChannel(Mockito.eq(portFilePath), Mockito.any(), Mockito.any())).thenReturn(portFileChannel); Mockito.when(portFileChannel.read(Mockito.any())).then(invocation -> { ByteBuffer buf = invocation.getArgument(0); From 245a9952033b72cb222e2187a638e9fd54bb6e1d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 30 Apr 2017 01:00:39 +0200 Subject: [PATCH 09/23] Filtering key events using Guava --- .../ui/controllers/SettingsController.java | 12 ++++++------ .../ui/controllers/UnlockController.java | 14 +++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index a2984623b..e0494ee37 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -14,11 +14,13 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; -import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.settings.Localization; +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; + import javafx.beans.binding.Bindings; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -34,6 +36,8 @@ import javafx.scene.layout.VBox; @Singleton public class SettingsController implements ViewController { + private static final CharMatcher DIGITS_MATCHER = CharMatcher.inRange('0', '9'); + private final Localization localization; private final Settings settings; private final Optional applicationVersion; @@ -130,11 +134,7 @@ public class SettingsController implements ViewController { } private void filterNumericKeyEvents(KeyEvent t) { - if (t.getCharacter() == null || t.getCharacter().length() == 0) { - return; - } - char c = CharUtils.toChar(t.getCharacter()); - if (!(CharUtils.isAsciiNumeric(c) || c == '_')) { + if (!Strings.isNullOrEmpty(t.getCharacter()) && !DIGITS_MATCHER.matchesAllOf(t.getCharacter())) { t.consume(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index a3b624c82..0c8f79434 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -31,6 +31,9 @@ import org.cryptomator.ui.util.DialogBuilderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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; @@ -55,6 +58,11 @@ import javafx.util.StringConverter; public class UnlockController implements ViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); + private static final CharMatcher ALPHA_NUMERIC_MATCHER = CharMatcher.inRange('a', 'z') // + .or(CharMatcher.inRange('A', 'Z')) // + .or(CharMatcher.inRange('0', '9')) // + .or(CharMatcher.is('_')) // + .precomputed(); private final Application app; private final Localization localization; @@ -212,11 +220,7 @@ public class UnlockController implements ViewController { } private void filterAlphanumericKeyEvents(KeyEvent t) { - if (t.getCharacter() == null || t.getCharacter().length() == 0) { - return; - } - char c = CharUtils.toChar(t.getCharacter()); - if (!(CharUtils.isAsciiAlphanumeric(c) || c == '_')) { + if (!Strings.isNullOrEmpty(t.getCharacter()) && !ALPHA_NUMERIC_MATCHER.matchesAllOf(t.getCharacter())) { t.consume(); } } From e7157a64edae9237f2b0a440eb0abe82018e59e4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 30 Apr 2017 09:51:10 +0200 Subject: [PATCH 10/23] removed slack build notifications [ci skip] --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83a72d52d..b2de1bb75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,11 +32,6 @@ notifications: on_success: change on_failure: always on_start: false - slack: - rooms: - secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs=" - on_success: change - on_failure: always before_deploy: - mvn -fmain/pom.xml -Prelease clean package -DskipTests deploy: From d2a2e2304dcca0fa7b7b546d2174f5914e1d6e65 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 4 May 2017 12:47:15 +0200 Subject: [PATCH 11/23] Implemented #40, tested on macOS --- .../cryptomator/common/LazyInitializer.java | 1 + .../cryptomator/common/settings/Settings.java | 11 ++- .../common/settings/SettingsJsonAdapter.java | 14 +-- .../common/settings/SettingsProvider.java | 5 +- .../common/settings/VaultSettings.java | 25 +++--- .../settings/VaultSettingsJsonAdapter.java | 14 ++- .../settings/SettingsJsonAdapterTest.java | 5 +- .../common/settings/SettingsTest.java | 33 +++++++ .../VaultSettingsJsonAdapterTest.java | 4 +- .../ui/controllers/MainController.java | 21 ++--- .../ui/controllers/UnlockController.java | 37 +++++--- .../cryptomator/ui/model/AutoUnlocker.java | 85 +++++++++++++++++++ .../java/org/cryptomator/ui/model/Vault.java | 21 ++--- .../cryptomator/ui/model/VaultFactory.java | 3 +- .../org/cryptomator/ui/model/VaultList.java | 42 +++++---- main/ui/src/main/resources/fxml/settings.fxml | 2 +- main/ui/src/main/resources/fxml/unlock.fxml | 20 +++-- .../ui/src/main/resources/localization/en.txt | 1 + 18 files changed, 245 insertions(+), 99 deletions(-) create mode 100644 main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java index 0d14ce339..de5d8abfd 100644 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java @@ -43,6 +43,7 @@ public final class LazyInitializer { try { return reference.updateAndGet(invokeFactoryIfNull(factory)); } catch (InitializationException e) { + Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e.getCause(), exceptionType); throw e; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java index a43c8cf83..cccd9808d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java @@ -34,20 +34,19 @@ public class Settings { public static final String DEFAULT_GVFS_SCHEME = "dav"; public static final boolean DEFAULT_DEBUG_MODE = false; - private final Consumer saveCmd; - private final ObservableList directories = FXCollections.observableArrayList(); + private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables); private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES); private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT); private final BooleanProperty useIpv6 = new SimpleBooleanProperty(DEFAULT_USE_IPV6); private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS); private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME); private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE); + private Consumer saveCmd; /** * Package-private constructor; use {@link SettingsProvider}. */ - Settings(Consumer saveCmd) { - this.saveCmd = saveCmd; + Settings() { directories.addListener((ListChangeListener.Change change) -> this.save()); checkForUpdates.addListener(this::somethingChanged); port.addListener(this::somethingChanged); @@ -57,6 +56,10 @@ public class Settings { debugMode.addListener(this::somethingChanged); } + void setSaveCmd(Consumer saveCmd) { + this.saveCmd = saveCmd; + } + private void somethingChanged(ObservableValue observable, Object oldValue, Object newValue) { this.save(); } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 1bb4ddbc8..cc1bc3792 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -8,7 +8,6 @@ package org.cryptomator.common.settings; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,13 +21,8 @@ public class SettingsJsonAdapter extends TypeAdapter { private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class); - private final Consumer saveCmd; private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter(); - public SettingsJsonAdapter(Consumer saveCmd) { - this.saveCmd = saveCmd; - } - @Override public void write(JsonWriter out, Settings value) throws IOException { out.beginObject(); @@ -53,14 +47,14 @@ public class SettingsJsonAdapter extends TypeAdapter { @Override public Settings read(JsonReader in) throws IOException { - Settings settings = new Settings(saveCmd); + Settings settings = new Settings(); in.beginObject(); while (in.hasNext()) { String name = in.nextName(); switch (name) { case "directories": - settings.getDirectories().addAll(readVaultSettingsArray(in, settings)); + settings.getDirectories().addAll(readVaultSettingsArray(in)); break; case "checkForUpdatesEnabled": settings.checkForUpdates().set(in.nextBoolean()); @@ -93,11 +87,11 @@ public class SettingsJsonAdapter extends TypeAdapter { return settings; } - private List readVaultSettingsArray(JsonReader in, Settings settings) throws IOException { + private List readVaultSettingsArray(JsonReader in) throws IOException { List result = new ArrayList<>(); in.beginArray(); while (!JsonToken.END_ARRAY.equals(in.peek())) { - result.add(vaultSettingsJsonAdapter.read(in, settings)); + result.add(vaultSettingsJsonAdapter.read(in)); } in.endArray(); return result; diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index b413a7e23..2974252db 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -62,7 +62,7 @@ public class SettingsProvider implements Provider { private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor(); private final AtomicReference> scheduledSaveCmd = new AtomicReference<>(); private final AtomicReference settings = new AtomicReference<>(); - private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(this::scheduleSave); + private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(); private final Gson gson; @Inject @@ -100,8 +100,9 @@ public class SettingsProvider implements Provider { LOG.info("Settings loaded from " + settingsPath); } catch (IOException e) { LOG.info("Failed to load settings, creating new one."); - settings = new Settings(this::scheduleSave); + settings = new Settings(); } + settings.setSaveCmd(this::scheduleSave); return settings; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 34e281aac..8ee82146f 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -15,41 +15,36 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.fxmisc.easybind.EasyBind; +import javafx.beans.Observable; 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; public class VaultSettings { + public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false; public static final boolean DEFAULT_MOUNT_AFTER_UNLOCK = true; public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true; - private final Settings settings; private final String id; private final ObjectProperty path = new SimpleObjectProperty<>(); private final StringProperty mountName = new SimpleStringProperty(); private final StringProperty winDriveLetter = new SimpleStringProperty(); + private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP); private final BooleanProperty mountAfterUnlock = new SimpleBooleanProperty(DEFAULT_MOUNT_AFTER_UNLOCK); private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT); - public VaultSettings(Settings settings, String id) { - this.settings = settings; + public VaultSettings(String id) { this.id = Objects.requireNonNull(id); EasyBind.subscribe(path, this::deriveMountNameFromPath); - path.addListener(this::somethingChanged); - mountName.addListener(this::somethingChanged); - winDriveLetter.addListener(this::somethingChanged); - mountAfterUnlock.addListener(this::somethingChanged); - revealAfterMount.addListener(this::somethingChanged); } - private void somethingChanged(ObservableValue observable, Object oldValue, Object newValue) { - settings.save(); + Observable[] observables() { + return new Observable[] {path, mountName, winDriveLetter, unlockAfterStartup, mountAfterUnlock, revealAfterMount}; } private void deriveMountNameFromPath(Path path) { @@ -58,8 +53,8 @@ public class VaultSettings { } } - public static VaultSettings withRandomId(Settings settings) { - return new VaultSettings(settings, generateId()); + public static VaultSettings withRandomId() { + return new VaultSettings(generateId()); } private static String generateId() { @@ -116,6 +111,10 @@ public class VaultSettings { return winDriveLetter; } + public BooleanProperty unlockAfterStartup() { + return unlockAfterStartup; + } + public BooleanProperty mountAfterUnlock() { return mountAfterUnlock; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 7fd9bc407..d1de6231e 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -24,18 +24,20 @@ class VaultSettingsJsonAdapter { out.name("path").value(value.path().get().toString()); out.name("mountName").value(value.mountName().get()); out.name("winDriveLetter").value(value.winDriveLetter().get()); + out.name("unlockAfterStartup").value(value.unlockAfterStartup().get()); out.name("mountAfterUnlock").value(value.mountAfterUnlock().get()); out.name("revealAfterMount").value(value.revealAfterMount().get()); out.endObject(); } - public VaultSettings read(JsonReader in, Settings settings) throws IOException { + public VaultSettings read(JsonReader in) throws IOException { String id = null; String path = null; String mountName = null; String winDriveLetter = null; - boolean mountAfterUnlock = true; - boolean revealAfterMount = true; + boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP; + boolean mountAfterUnlock = VaultSettings.DEFAULT_MOUNT_AFTER_UNLOCK; + boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT; in.beginObject(); while (in.hasNext()) { @@ -53,6 +55,9 @@ class VaultSettingsJsonAdapter { case "winDriveLetter": winDriveLetter = in.nextString(); break; + case "unlockAfterStartup": + unlockAfterStartup = in.nextBoolean(); + break; case "mountAfterUnlock": mountAfterUnlock = in.nextBoolean(); break; @@ -66,10 +71,11 @@ class VaultSettingsJsonAdapter { } in.endObject(); - VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId(settings) : new VaultSettings(settings, id); + VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id); vaultSettings.mountName().set(mountName); vaultSettings.path().set(Paths.get(path)); vaultSettings.winDriveLetter().set(winDriveLetter); + vaultSettings.unlockAfterStartup().set(unlockAfterStartup); vaultSettings.mountAfterUnlock().set(mountAfterUnlock); vaultSettings.revealAfterMount().set(revealAfterMount); return vaultSettings; diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java index 6b32a06b3..23ddaab1d 100644 --- a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java @@ -12,10 +12,7 @@ import org.junit.Test; public class SettingsJsonAdapterTest { - private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(this::noop); - - private void noop(Settings settings) { - } + private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(); @Test public void testDeserialize() throws IOException { diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java new file mode 100644 index 000000000..fcce53253 --- /dev/null +++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java @@ -0,0 +1,33 @@ +package org.cryptomator.common.settings; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.junit.Test; +import org.mockito.Mockito; + +public class SettingsTest { + + @Test + public void testAutoSave() throws IOException { + @SuppressWarnings("unchecked") + Consumer changeListener = Mockito.mock(Consumer.class); + Settings settings = new Settings(); + settings.setSaveCmd(changeListener); + VaultSettings vaultSettings = VaultSettings.withRandomId(); + Mockito.verify(changeListener, Mockito.times(0)).accept(settings); + + // first change (to property): + settings.preferredGvfsScheme().set("asd"); + Mockito.verify(changeListener, Mockito.times(1)).accept(settings); + + // second change (to list): + settings.getDirectories().add(vaultSettings); + Mockito.verify(changeListener, Mockito.times(2)).accept(settings); + + // third change (to property of list item): + vaultSettings.mountName().set("asd"); + Mockito.verify(changeListener, Mockito.times(3)).accept(settings); + } + +} diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java index 0c2604e10..7320b6768 100644 --- a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java @@ -11,7 +11,6 @@ import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; import com.google.gson.stream.JsonReader; @@ -23,9 +22,8 @@ public class VaultSettingsJsonAdapterTest { public void testDeserialize() throws IOException { String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true}"; JsonReader jsonReader = new JsonReader(new StringReader(json)); - Settings settings = Mockito.mock(Settings.class); - VaultSettings vaultSettings = adapter.read(jsonReader, settings); + VaultSettings vaultSettings = adapter.read(jsonReader); Assert.assertEquals("foo", vaultSettings.getId()); Assert.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get()); Assert.assertEquals("test", vaultSettings.mountName().get()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 80223b92f..96eaaeec6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -26,10 +26,10 @@ import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.ui.ExitUtil; import org.cryptomator.ui.controls.DirectoryListCell; +import org.cryptomator.ui.model.AutoUnlocker; import org.cryptomator.ui.model.UpgradeStrategies; import org.cryptomator.ui.model.UpgradeStrategy; import org.cryptomator.ui.model.Vault; @@ -50,7 +50,9 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -83,11 +85,11 @@ public class MainController implements ViewController { private final Localization localization; private final ExecutorService executorService; private final BlockingQueue fileOpenRequests; - private final Settings settings; private final VaultFactory vaultFactoy; private final ViewControllerLoader viewControllerLoader; private final ObjectProperty activeController = new SimpleObjectProperty<>(); - private final VaultList vaults; + private final ObservableList vaults; + private final BooleanBinding areAllVaultsLocked; private final ObjectProperty selectedVault = new SimpleObjectProperty<>(); private final BooleanExpression isSelectedVaultUnlocked = BooleanExpression.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false)); private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false)); @@ -100,13 +102,12 @@ public class MainController implements ViewController { @Inject public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue fileOpenRequests, ExitUtil exitUtil, Localization localization, - Settings settings, VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults) { + VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) { this.mainWindow = mainWindow; this.executorService = executorService; this.fileOpenRequests = fileOpenRequests; this.exitUtil = exitUtil; this.localization = localization; - this.settings = settings; this.vaultFactoy = vaultFactoy; this.viewControllerLoader = viewControllerLoader; this.vaults = vaults; @@ -114,6 +115,10 @@ public class MainController implements ViewController { // derived bindings: this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass)); this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy); + this.areAllVaultsLocked = new SimpleBooleanProperty(false).not(); // = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault::isUnlocked)); + + EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit); + autoUnlocker.unlockAllSilently(); } @FXML @@ -281,7 +286,7 @@ public class MainController implements ViewController { } final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> { - VaultSettings vaultSettings = VaultSettings.withRandomId(settings); + VaultSettings vaultSettings = VaultSettings.withRandomId(); vaultSettings.path().set(vaultPath); return vaultFactoy.get(vaultSettings); }); @@ -399,7 +404,6 @@ public class MainController implements ViewController { } public void didUnlock(Vault vault) { - Platform.setImplicitExit(false); if (vault.equals(selectedVault.getValue())) { this.showUnlockedView(vault); } @@ -417,9 +421,6 @@ public class MainController implements ViewController { public void didLock(UnlockedController ctrl) { unlockedVaults.remove(ctrl.getVault()); showUnlockView(); - if (!vaults.stream().anyMatch(Vault::isUnlocked)) { - Platform.setImplicitExit(true); - } } private void showChangePasswordView() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 0c8f79434..ffaad54cc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -17,6 +17,7 @@ import javax.inject.Inject; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.cryptomator.frontend.webdav.ServerLifecycleException; @@ -28,6 +29,8 @@ import org.cryptomator.ui.model.WindowsDriveLetters; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DialogBuilderUtil; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +75,7 @@ public class UnlockController implements ViewController { private final Optional keychainAccess; private Vault vault; private Optional listener = Optional.empty(); + private Subscription vaultSubs = Subscription.EMPTY; @Inject public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, WindowsDriveLetters driveLetters, Optional keychainAccess) { @@ -124,6 +128,9 @@ public class UnlockController implements ViewController { @FXML private GridPane root; + @FXML + private CheckBox unlockAfterStartup; + @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); @@ -133,6 +140,7 @@ public class UnlockController implements ViewController { mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); savePassword.setDisable(!keychainAccess.isPresent()); + unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not())); if (SystemUtils.IS_OS_WINDOWS) { winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); } else { @@ -149,18 +157,17 @@ public class UnlockController implements ViewController { } void setVault(Vault vault) { - // TODO overheadhunter refactor - if (this.vault != null) { - this.vault.getVaultSettings().mountAfterUnlock().unbind(); - this.vault.getVaultSettings().revealAfterMount().unbind(); - } + vaultSubs.unsubscribe(); + vaultSubs = Subscription.EMPTY; + // trigger "default" change to refresh key bindings: unlockButton.setDefaultButton(false); unlockButton.setDefaultButton(true); - if (vault.equals(this.vault)) { + if (Objects.equals(this.vault, Objects.requireNonNull(vault))) { return; } - this.vault = Objects.requireNonNull(vault); + assert vault != null; + this.vault = vault; passwordField.swipe(); advancedOptions.setVisible(false); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); @@ -190,10 +197,18 @@ public class UnlockController implements ViewController { Arrays.fill(storedPw, ' '); } } - mountAfterUnlock.setSelected(this.vault.getVaultSettings().mountAfterUnlock().get()); - revealAfterMount.setSelected(this.vault.getVaultSettings().revealAfterMount().get()); - this.vault.getVaultSettings().mountAfterUnlock().bind(mountAfterUnlock.selectedProperty()); - this.vault.getVaultSettings().revealAfterMount().bind(revealAfterMount.selectedProperty()); + VaultSettings settings = vault.getVaultSettings(); + unlockAfterStartup.setSelected(savePassword.isSelected() && settings.unlockAfterStartup().get()); + mountAfterUnlock.setSelected(settings.mountAfterUnlock().get()); + revealAfterMount.setSelected(settings.revealAfterMount().get()); + + // settings.unlockAfterStartup().bind(unlockAfterStartup.selectedProperty()); + // settings.mountAfterUnlock().bind(mountAfterUnlock.selectedProperty()); + // settings.revealAfterMount().bind(revealAfterMount.selectedProperty()); + + vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), settings.unlockAfterStartup()::set)); + vaultSubs = vaultSubs.and(EasyBind.subscribe(mountAfterUnlock.selectedProperty(), settings.mountAfterUnlock()::set)); + vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), settings.revealAfterMount()::set)); } // **************************************** diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java b/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java new file mode 100644 index 000000000..c947aa53d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java @@ -0,0 +1,85 @@ +package org.cryptomator.ui.model; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException; +import org.cryptomator.keychain.KeychainAccess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class AutoUnlocker { + + private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class); + + private final Optional keychainAccess; + private final VaultList vaults; + private final ExecutorService executor; + + @Inject + public AutoUnlocker(Optional keychainAccess, VaultList vaults, ExecutorService executor) { + this.keychainAccess = keychainAccess; + this.vaults = vaults; + this.executor = executor; + } + + public void unlockAllSilently() { + if (keychainAccess.isPresent()) { + vaults.stream().filter(this::shouldUnlockAfterStartup).map(this::createUnlockTask).forEach(executor::submit); + } + } + + private boolean shouldUnlockAfterStartup(Vault vault) { + return vault.getVaultSettings().unlockAfterStartup().get(); + } + + private Runnable createUnlockTask(Vault vault) { + return () -> unlockSilently(vault); + } + + private void unlockSilently(Vault vault) { + char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + if (storedPw == null) { + LOG.warn("No passphrase stored in keychain for vault registered for auto unlocking: {}", vault.getPath()); + } + try { + vault.unlock(CharBuffer.wrap(storedPw)); + mountSilently(vault); + } catch (CryptoException e) { + LOG.error("Auto unlock failed.", e); + } finally { + Arrays.fill(storedPw, ' '); + } + } + + private void mountSilently(Vault unlockedVault) { + if (!unlockedVault.getVaultSettings().mountAfterUnlock().get()) { + return; + } + try { + unlockedVault.mount(); + revealSilently(unlockedVault); + } catch (CommandFailedException e) { + LOG.error("Auto unlock succeded, but mounting the drive failed.", e); + } + } + + private void revealSilently(Vault mountedVault) { + if (!mountedVault.getVaultSettings().revealAfterMount().get()) { + return; + } + try { + mountedVault.reveal(); + } catch (CommandFailedException e) { + LOG.error("Auto unlock succeded, but revealing the drive failed.", e); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index d972c3ab6..a9be7c3d3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Platform; +import javafx.beans.Observable; import javafx.beans.binding.Binding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -77,7 +78,7 @@ public class Vault { // Commands // ********************************************************************************/ - private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException { + private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException { return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class); } @@ -126,7 +127,7 @@ public class Vault { } } - public synchronized void mount() { + public synchronized void mount() throws CommandFailedException { if (servlet == null) { throw new IllegalStateException("Mounting requires unlocked WebDAV servlet."); } @@ -136,14 +137,10 @@ public class Vault { .withPreferredGvfsScheme(settings.preferredGvfsScheme().get()) // .build(); - try { - mount = servlet.mount(mountParams); - Platform.runLater(() -> { - mounted.set(true); - }); - } catch (CommandFailedException e) { - LOG.error("Unable to mount filesystem", e); - } + mount = servlet.mount(mountParams); + Platform.runLater(() -> { + mounted.set(true); + }); } public synchronized void unmount() throws Exception { @@ -190,6 +187,10 @@ public class Vault { // Getter/Setter // *******************************************************************************/ + public Observable[] observables() { + return new Observable[] {unlockedProperty(), mountedProperty()}; + } + public VaultSettings getVaultSettings() { return vaultSettings; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java index 7b4b553e2..824b3cc1d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java @@ -15,12 +15,11 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.cryptomator.common.settings.VaultSettings; -import org.cryptomator.ui.model.VaultComponent.Builder; @Singleton public class VaultFactory { - private final Builder vaultComponentBuilder; + private final VaultComponent.Builder vaultComponentBuilder; private final ConcurrentMap vaults = new ConcurrentHashMap<>(); @Inject diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java index 519fea90f..f2c9718fc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java @@ -5,8 +5,8 @@ *******************************************************************************/ package org.cryptomator.ui.model; -import java.util.ArrayList; import java.util.List; +import java.util.stream.IntStream; import javax.inject.Inject; import javax.inject.Singleton; @@ -14,7 +14,9 @@ import javax.inject.Singleton; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; -import javafx.collections.ListChangeListener.Change; +import com.google.common.collect.Lists; + +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.TransformationList; @@ -59,15 +61,15 @@ public class VaultList extends TransformationList { } @Override - protected void sourceChanged(Change c) { + protected void sourceChanged(ListChangeListener.Change c) { this.fireChange(new VaultListChange(c)); } - private class VaultListChange extends Change { + private class VaultListChange extends ListChangeListener.Change { - private final Change delegate; + private final ListChangeListener.Change delegate; - public VaultListChange(Change delegate) { + public VaultListChange(ListChangeListener.Change delegate) { super(VaultList.this); this.delegate = delegate; } @@ -77,6 +79,11 @@ public class VaultList extends TransformationList { return delegate.next(); } + @Override + public boolean wasUpdated() { + return delegate.wasUpdated(); + } + @Override public void reset() { delegate.reset(); @@ -94,27 +101,28 @@ public class VaultList extends TransformationList { @Override public List getRemoved() { - List removed = new ArrayList<>(); - for (VaultSettings s : delegate.getRemoved()) { - removed.add(vaultFactory.get(s)); - } - return removed; + return Lists.transform(delegate.getRemoved(), vaultFactory::get); + } + + @Override + public boolean wasPermutated() { + return delegate.wasPermutated(); } @Override protected int[] getPermutation() { if (delegate.wasPermutated()) { - int len = getTo() - getFrom(); - int[] permutations = new int[len]; - for (int i = 0; i < len; i++) { - permutations[i] = getPermutation(i); - } - return permutations; + return IntStream.range(getFrom(), getTo()).map(delegate::getPermutation).toArray(); } else { return new int[0]; } } + @Override + public String toString() { + return delegate.toString(); + } + } } diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index 65199e284..4f7fc3bfe 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -53,7 +53,7 @@ -