From 3f32e4ee4beaeba3bc0bc849112c7f787a0973e3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 24 Dec 2014 14:10:30 +0100 Subject: [PATCH] - Fixed initial encryption of vaults, that already contain files - Disabled some UI controls during background tasks - Simplified background vs UI thread switches using https://github.com/totalvoidness/FXThreads --- .../files/EncryptingFileVisitor.java | 23 ++- .../cryptomator/ui/InitializeController.java | 45 ++++- .../org/cryptomator/ui/UnlockController.java | 40 ++-- .../org/cryptomator/ui/model/Directory.java | 20 +- .../org/cryptomator/ui/util/FXThreads.java | 175 ++++++++++++++++++ .../src/main/resources/fxml/initialize.fxml | 3 + main/ui/src/main/resources/fxml/unlock.fxml | 2 +- 7 files changed, 260 insertions(+), 48 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java diff --git a/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java b/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java index ebe12b8ad..f6e476b18 100644 --- a/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java +++ b/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java @@ -1,6 +1,8 @@ package org.cryptomator.files; import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.SeekableByteChannel; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -24,7 +26,7 @@ public class EncryptingFileVisitor extends SimpleFileVisitor implements Cr this.cryptor = cryptor; this.encryptionDecider = encryptionDecider; } - + @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) { @@ -36,12 +38,15 @@ public class EncryptingFileVisitor extends SimpleFileVisitor implements Cr } @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (encryptionDecider.shouldEncrypt(file)) { - final String plaintext = file.getFileName().toString(); - final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this); - final Path newPath = file.resolveSibling(encrypted); - Files.move(file, newPath, StandardCopyOption.ATOMIC_MOVE); + public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException { + if (encryptionDecider.shouldEncrypt(plaintextFile)) { + final String plaintextName = plaintextFile.getFileName().toString(); + final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this); + final Path encryptedPath = plaintextFile.resolveSibling(encryptedName); + final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ); + final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + cryptor.encryptFile(plaintextIn, ciphertextOut); + Files.delete(plaintextFile); } return FileVisitResult.CONTINUE; } @@ -68,9 +73,9 @@ public class EncryptingFileVisitor extends SimpleFileVisitor implements Cr final Path path = currentDir.resolve(metadataFile); return Files.readAllBytes(path); } - + /* callback */ - + public interface EncryptionDecider { boolean shouldEncrypt(Path path); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java index 93094b40e..694986dce 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Optional; import java.util.ResourceBundle; +import java.util.concurrent.Future; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; @@ -30,6 +31,7 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; @@ -41,6 +43,7 @@ import org.cryptomator.files.EncryptingFileVisitor; import org.cryptomator.ui.controls.ClearOnDisableListener; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Directory; +import org.cryptomator.ui.util.FXThreads; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +68,9 @@ public class InitializeController implements Initializable { @FXML private Button okButton; + @FXML + private ProgressIndicator progressIndicator; + @FXML private Label messageLabel; @@ -123,6 +129,7 @@ public class InitializeController implements Initializable { @FXML protected void initializeVault(ActionEvent event) { + setControlsDisabled(true); if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) { return; } @@ -131,18 +138,29 @@ public class InitializeController implements Initializable { final CharSequence password = passwordField.getCharacters(); OutputStream masterKeyOutputStream = null; try { + progressIndicator.setVisible(true); masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password); - encryptExistingContents(); - directory.getCryptor().swipeSensitiveData(); - if (listener != null) { - listener.didInitialize(this); - } + final Future futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents); + FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> { + progressIndicator.setVisible(false); + progressIndicator.setVisible(false); + directory.getCryptor().swipeSensitiveData(); + if (listener != null) { + listener.didInitialize(this); + } + }); } catch (FileAlreadyExistsException ex) { + setControlsDisabled(false); + progressIndicator.setVisible(false); messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); } catch (InvalidPathException ex) { + setControlsDisabled(false); + progressIndicator.setVisible(false); messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath")); } catch (IOException ex) { + setControlsDisabled(false); + progressIndicator.setVisible(false); LOG.error("I/O Exception", ex); } finally { usernameField.setText(null); @@ -152,6 +170,13 @@ public class InitializeController implements Initializable { } } + private void setControlsDisabled(boolean disable) { + usernameField.setDisable(disable); + passwordField.setDisable(disable); + retypePasswordField.setDisable(disable); + okButton.setDisable(disable); + } + private boolean isDirectoryEmpty() { try { final DirectoryStream dirContents = Files.newDirectoryStream(directory.getPath()); @@ -172,9 +197,13 @@ public class InitializeController implements Initializable { return ButtonType.OK.equals(result.get()); } - private void encryptExistingContents() throws IOException { - final FileVisitor visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile); - Files.walkFileTree(directory.getPath(), visitor); + private void encryptExistingContents() { + try { + final FileVisitor visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile); + Files.walkFileTree(directory.getPath(), visitor); + } catch (IOException ex) { + LOG.error("I/O Exception", ex); + } } private boolean shouldEncryptExistingFile(Path path) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java index ad629ad85..2dfb04894 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java @@ -16,12 +16,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; +import java.util.concurrent.Future; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; @@ -34,6 +36,7 @@ import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Directory; +import org.cryptomator.ui.util.FXThreads; import org.cryptomator.ui.util.MasterKeyFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +54,10 @@ public class UnlockController implements Initializable { @FXML private SecPasswordField passwordField; - + + @FXML + private Button unlockButton; + @FXML private ProgressIndicator progressIndicator; @@ -82,6 +88,7 @@ public class UnlockController implements Initializable { @FXML private void didClickUnlockButton(ActionEvent event) { + setControlsDisabled(true); final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT; final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName); final CharSequence password = passwordField.getCharacters(); @@ -96,15 +103,23 @@ public class UnlockController implements Initializable { return; } directory.setUnlocked(true); - directory.mountAsync(this::didUnlockAndMount); + final Future futureMount = FXThreads.runOnBackgroundThread(directory::mount); + FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount); + FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> { + setControlsDisabled(false); + }); } catch (DecryptFailedException | IOException ex) { + setControlsDisabled(false); progressIndicator.setVisible(false); messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed")); LOG.error("Decryption failed for technical reasons.", ex); } catch (WrongPasswordException e) { + setControlsDisabled(false); progressIndicator.setVisible(false); messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword")); + passwordField.requestFocus(); } catch (UnsupportedKeyLengthException ex) { + setControlsDisabled(false); progressIndicator.setVisible(false); messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE")); LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); @@ -114,6 +129,12 @@ public class UnlockController implements Initializable { } } + private void setControlsDisabled(boolean disable) { + usernameBox.setDisable(disable); + passwordField.setDisable(disable); + unlockButton.setDisable(disable); + } + private void findExistingUsernames() { try { DirectoryStream ds = MasterKeyFilter.filteredDirectory(directory.getPath()); @@ -132,15 +153,12 @@ public class UnlockController implements Initializable { LOG.trace("Invalid path: " + directory.getPath(), e); } } - - private Void didUnlockAndMount(boolean mountSuccess) { - Platform.runLater(() -> { - progressIndicator.setVisible(false); - if (listener != null) { - listener.didUnlock(this); - } - }); - return null; + + private void didUnlockAndMount(boolean mountSuccess) { + progressIndicator.setVisible(false); + if (listener != null) { + listener.didUnlock(this); + } } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java index 3e9ebc058..66112bc6e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -4,13 +4,9 @@ import java.io.IOException; import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.util.Callback; import org.cryptomator.crypto.Cryptor; import org.cryptomator.crypto.SamplingDecorator; @@ -71,21 +67,7 @@ public class Directory implements Serializable { } } - public void mountAsync(Callback callback) { - final FutureTask mountTask = new FutureTask<>(this::mount); - final Executor exec = Executors.newSingleThreadExecutor(); - exec.execute(mountTask); - exec.execute(() -> { - try { - final Boolean result = mountTask.get(); - callback.call(result); - } catch (Exception e) { - callback.call(false); - } - }); - } - - private boolean mount() { + public boolean mount() { try { webDavMount = WebDavMounter.mount(server.getPort()); return true; diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java b/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java new file mode 100644 index 000000000..86a5a18dd --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (c) 2014 Sebastian Stenzel + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * https://github.com/totalvoidness/FXThreads + * + * Contributors: + * Sebastian Stenzel + ******************************************************************************/ +package org.cryptomator.ui.util; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javafx.application.Platform; + +/** + * Use this utility class to spawn background tasks and wait for them to finish.
+ *
+ * Example use (ignoring exceptions): + * + *
+ * // get some string from a remote server:
+ * Future<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
+ * 
+ * // when done, update text label:
+ * runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
+ * 	myLabel.setText(bookName);
+ * });
+ * 
+ * + * Example use (exception-aware): + * + *
+ * // get some string from a remote server:
+ * Future<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
+ * 
+ * // when done, update text label:
+ * runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
+ * 	myLabel.setText(bookName);
+ * }, (exception) -> {
+ * 	myLabel.setText("An exception occured: " + exception.getMessage());
+ * });
+ * 
+ */ +public final class FXThreads { + + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); + private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> { + // ignore. + }; + + private FXThreads() { + throw new AssertionError("Not instantiable."); + } + + /** + * Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use + * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}. + * + *
+	 * // examples:
+	 * 
+	 * Future<String> futureBookName1 = runOnBackgroundThread(restResource::getBookName);
+	 * 
+	 * Future<String> futureBookName2 = runOnBackgroundThread(() -> {
+	 * 	return restResource.getBookName();
+	 * });
+	 * 
+ * + * @param task The task to be executed on a background thread. + * @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}. + */ + public static Future runOnBackgroundThread(Callable task) { + return EXECUTOR.submit(task); + } + + /** + * Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use + * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}. + * + *
+	 * // examples:
+	 * 
+	 * Future<?> futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
+	 * 
+	 * Future<?> futureDone2 = runOnBackgroundThread(() -> {
+	 * 	doSomeComplexCalculation();
+	 * });
+	 * 
+ * + * @param task The task to be executed on a background thread. + * @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}. + */ + public static Future runOnBackgroundThread(Runnable task) { + return EXECUTOR.submit(task); + } + + /** + * Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be + * called. If you are interested in the exception, use + * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead. + * + *
+	 * // example:
+	 * 
+	 * runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
+	 * 	myLabel.setText(bookName);
+	 * });
+	 * 
+ * + * @param task The task to wait for. + * @param successCallback The action to perform, when the task finished. + */ + public static void runOnMainThreadWhenFinished(Future task, CallbackWhenTaskFinished successCallback) { + runOnBackgroundThread(() -> { + return "asd"; + }); + FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK); + } + + /** + * Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be + * called. If you are interested in the exception, use + * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead. + * + *
+	 * // example:
+	 * 
+	 * runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -> {
+	 * 	myLabel.setText(bookName);
+	 * }, (exception) -> {
+	 * 	myLabel.setText("An exception occured: " + exception.getMessage());
+	 * });
+	 * 
+ * + * @param task The task to wait for. + * @param successCallback The action to perform, when the task finished. + */ + public static void runOnMainThreadWhenFinished(Future task, CallbackWhenTaskFinished successCallback, CallbackWhenTaskFailed exceptionCallback) { + assertParamNotNull(task, "task must not be null."); + assertParamNotNull(successCallback, "successCallback must not be null."); + assertParamNotNull(exceptionCallback, "exceptionCallback must not be null."); + EXECUTOR.execute(() -> { + try { + final T result = task.get(); + Platform.runLater(() -> { + successCallback.taskFinished(result); + }); + } catch (Exception e) { + Platform.runLater(() -> { + exceptionCallback.taskFailed(e); + }); + } + }); + } + + private static void assertParamNotNull(Object param, String msg) { + if (param == null) { + throw new IllegalArgumentException(msg); + } + } + + public interface CallbackWhenTaskFinished { + void taskFinished(T result); + } + + public interface CallbackWhenTaskFailed { + void taskFailed(Throwable t); + } + +} diff --git a/main/ui/src/main/resources/fxml/initialize.fxml b/main/ui/src/main/resources/fxml/initialize.fxml index 3ebbf7a0e..65edc25d1 100644 --- a/main/ui/src/main/resources/fxml/initialize.fxml +++ b/main/ui/src/main/resources/fxml/initialize.fxml @@ -44,6 +44,9 @@