Преглед на файлове

- 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

Sebastian Stenzel преди 10 години
родител
ревизия
3f32e4ee4b

+ 14 - 9
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<Path> 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<Path> 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<Path> implements Cr
 		final Path path = currentDir.resolve(metadataFile);
 		return Files.readAllBytes(path);
 	}
-	
+
 	/* callback */
-	
+
 	public interface EncryptionDecider {
 		boolean shouldEncrypt(Path path);
 	}

+ 37 - 8
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<Path> 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<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
-		Files.walkFileTree(directory.getPath(), visitor);
+	private void encryptExistingContents() {
+		try {
+			final FileVisitor<Path> 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) {

+ 29 - 11
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<Boolean> 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<Path> 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 */

+ 1 - 19
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<Boolean, Void> callback) {
-		final FutureTask<Boolean> 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;

+ 175 - 0
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. <br/>
+ * <br/>
+ * <strong>Example use (ignoring exceptions):</strong>
+ * 
+ * <pre>
+ * // get some string from a remote server:
+ * Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
+ * 
+ * // when done, update text label:
+ * runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
+ * 	myLabel.setText(bookName);
+ * });
+ * </pre>
+ * 
+ * <strong>Example use (exception-aware):</strong>
+ * 
+ * <pre>
+ * // get some string from a remote server:
+ * Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
+ * 
+ * // when done, update text label:
+ * runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
+ * 	myLabel.setText(bookName);
+ * }, (exception) -&gt; {
+ * 	myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
+ * });
+ * </pre>
+ */
+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)}.
+	 * 
+	 * <pre>
+	 * // examples:
+	 * 
+	 * Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
+	 * 
+	 * Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
+	 * 	return restResource.getBookName();
+	 * });
+	 * </pre>
+	 * 
+	 * @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 <T> Future<T> runOnBackgroundThread(Callable<T> 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)}.
+	 * 
+	 * <pre>
+	 * // examples:
+	 * 
+	 * Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
+	 * 
+	 * Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
+	 * 	doSomeComplexCalculation();
+	 * });
+	 * </pre>
+	 * 
+	 * @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.
+	 * 
+	 * <pre>
+	 * // example:
+	 * 
+	 * runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
+	 * 	myLabel.setText(bookName);
+	 * });
+	 * </pre>
+	 * 
+	 * @param task The task to wait for.
+	 * @param successCallback The action to perform, when the task finished.
+	 */
+	public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> 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.
+	 * 
+	 * <pre>
+	 * // example:
+	 * 
+	 * runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -&gt; {
+	 * 	myLabel.setText(bookName);
+	 * }, (exception) -&gt; {
+	 * 	myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
+	 * });
+	 * </pre>
+	 * 
+	 * @param task The task to wait for.
+	 * @param successCallback The action to perform, when the task finished.
+	 */
+	public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> 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<T> {
+		void taskFinished(T result);
+	}
+
+	public interface CallbackWhenTaskFailed {
+		void taskFailed(Throwable t);
+	}
+
+}

+ 3 - 0
main/ui/src/main/resources/fxml/initialize.fxml

@@ -44,6 +44,9 @@
 		<!-- Row 3 -->
 		<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
 		
+		<!-- Row 4 -->
+		<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
+		
 		<!-- Row 5 -->
 		<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
 	</children>

+ 1 - 1
main/ui/src/main/resources/fxml/unlock.fxml

@@ -38,7 +38,7 @@
 		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
 		
 		<!-- Row 2 -->
-		<Button text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" focusTraversable="false"/>
+		<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
 		
 		<!-- Row 3 -->
 		<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>