refactored "add vault" functionality, which fixes #14

removed some dependencies
refactored Main/MainApplication, which fixes #16
This commit is contained in:
Sebastian Stenzel
2015-02-13 19:46:07 +01:00
parent 751dbe6b7e
commit 5e0ebab587
22 changed files with 325 additions and 352 deletions

View File

@@ -1,83 +0,0 @@
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;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
private final Path rootDir;
private final Cryptor cryptor;
private final EncryptionDecider encryptionDecider;
private Path currentDir;
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
this.rootDir = rootDir;
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
this.currentDir = dir;
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
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;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (encryptionDecider.shouldEncrypt(dir)) {
final String plaintext = dir.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = dir.resolveSibling(encrypted);
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
}
return FileVisitResult.CONTINUE;
}
@Override
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
final Path path = currentDir.resolve(metadataFile);
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}
}

View File

@@ -63,8 +63,4 @@ interface FileNamingConventions {
*/
PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
/**
* On OSX, folders with this extension are treated as a package.
*/
String FOLDER_EXTENSION = ".cryptomator";
}

View File

@@ -50,7 +50,7 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- UI -->
<!-- UI
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
@@ -60,7 +60,7 @@
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>desktop</artifactId>
</dependency>
</dependency> -->
</dependencies>
<build>

View File

@@ -11,39 +11,28 @@ package org.cryptomator.ui;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
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;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
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;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
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.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,7 +42,7 @@ public class InitializeController implements Initializable {
private static final int MAX_USERNAME_LENGTH = 250;
private ResourceBundle localization;
private Directory directory;
private Vault directory;
private InitializationListener listener;
@FXML
@@ -68,9 +57,6 @@ public class InitializeController implements Initializable {
@FXML
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@@ -130,43 +116,25 @@ public class InitializeController implements Initializable {
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
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);
}
});
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 {
setControlsDisabled(false);
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
@@ -177,47 +145,13 @@ public class InitializeController implements Initializable {
okButton.setDisable(disable);
}
private boolean isDirectoryEmpty() {
try {
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
return !dirContents.iterator().hasNext();
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
throw new IllegalStateException(e);
}
}
private boolean shouldEncryptExistingFiles() {
final Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
alert.setHeaderText(null);
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
final Optional<ButtonType> result = alert.showAndWait();
return ButtonType.OK.equals(result.get());
}
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) {
final String name = path.getFileName().toString();
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
}
/* Getter/Setter */
public Directory getDirectory() {
public Vault getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
public void setDirectory(Vault directory) {
this.directory = directory;
}

View File

@@ -5,11 +5,17 @@
*
* Contributors:
* Tillmann Gaida - initial implementation
* Sebastian Stenzel - refactoring
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -18,35 +24,40 @@ import javafx.application.Application;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.axet.desktop.os.mac.AppleHandlers;
public class Main {
public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
if (SystemUtils.IS_OS_MAC_OSX) {
/*
* On OSX we're in an awkward position. We need to register a
* handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a
* static CompletableFuture for the handler, which actually opens
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
AppleHandlers.getAppleHandlers().addOpenFileListener(file -> {
try {
OPEN_FILE_HANDLER.get().accept(file);
} catch (Exception e) {
LOG.error("exception handling file open event", e);
throw new RuntimeException(e);
}
});
} catch (RuntimeException e) {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
@@ -54,9 +65,13 @@ public class Main {
}
/*
* Before starting the application, we check if there is already an
* instance running on this computer. If so, we send our command line
* arguments to that instance and quit.
* Perform certain things on VM termination.
*/
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
/*
* Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
* line arguments to that instance and quit.
*/
final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
@@ -64,7 +79,7 @@ public class Main {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 1000);
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
@@ -73,4 +88,62 @@ public class Main {
Application.launch(MainApplication.class, args);
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
LOG.debug("Shutting down");
SHUTDOWN_TASKS.forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("exception while shutting down", e);
}
});
SHUTDOWN_TASKS.clear();
}
}
private static void handleOpenFileRequest(File file) {
try {
OPEN_FILE_HANDLER.get().accept(file);
} catch (Exception e) {
LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
throw new RuntimeException(e);
}
}
/**
* Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private static class OpenFilesHandlerClassHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
for (File f : ff) {
handleOpenFileRequest(f);
}
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}

View File

@@ -13,7 +13,6 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -24,24 +23,19 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.TrayIconUtil;
import org.cryptomator.webdav.WebDavServer;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainApplication extends Application {
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static final String APPLICATION_KEY = "CryptomatorGUI";
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
@@ -53,21 +47,15 @@ public class MainApplication extends Application {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads
* to no context class loader being set in the AppKit (event) thread
* if the application is not started opening a file.
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
Runtime.getRuntime().addShutdownHook(MainApplication.CLEAN_SHUTDOWN_PERFORMER);
executorService = Executors.newCachedThreadPool();
addShutdownTask(() -> {
executorService.shutdown();
});
WebDavServer.getInstance().start();
chooseNativeStylesheet();
@@ -91,40 +79,42 @@ public class MainApplication extends Application {
handleCommandLineArg(ctrl, arg);
}
if (org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.OSX)) {
if (SystemUtils.IS_OS_MAC_OSX) {
Main.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
}
LocalInstance cryptomatorGuiInstance = SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService);
addShutdownTask(() -> {
cryptomatorGuiInstance.close();
});
final LocalInstance cryptomatorGuiInstance = SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService);
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
Main.addShutdownTask(() -> {
cryptomatorGuiInstance.close();
Settings.save();
executorService.shutdown();
});
}
void handleCommandLineArg(final MainController ctrl, String arg) {
Path file = FileSystems.getDefault().getPath(arg);
if (!Files.exists(file)) {
try {
if (!Files.isDirectory(Files.createDirectories(file))) {
return;
}
} catch (IOException e) {
return;
}
// directory created.
} else if (Files.isRegularFile(file)) {
if (StringUtils.endsWithIgnoreCase(file.getFileName().toString(), Aes256Cryptor.MASTERKEY_FILE_EXT)) {
file = file.getParent();
} else {
// is a file, but not a masterkey file
return;
}
// only open files with our file extension:
if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
LOG.warn("Invalid vault path %s", arg);
return;
}
Path f = file;
// find correct location:
final Path path = FileSystems.getDefault().getPath(arg);
final Path vaultPath;
if (Files.isDirectory(path)) {
vaultPath = path;
} else if (Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultPath = path.getParent();
} else {
LOG.warn("Invalid vault path %s", arg);
return;
}
// add vault to ctrl:
Platform.runLater(() -> {
ctrl.addDirectory(f);
ctrl.addVault(vaultPath, true);
ctrl.toFront();
});
}
@@ -142,39 +132,10 @@ public class MainApplication extends Application {
private void quit() {
Platform.runLater(() -> {
WebDavServer.getInstance().stop();
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
Platform.exit();
System.exit(0);
});
}
@Override
public void stop() {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
SHUTDOWN_TASKS.forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("exception while shutting down", e);
}
});
SHUTDOWN_TASKS.clear();
}
}
}

View File

@@ -14,6 +14,7 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
@@ -25,20 +26,23 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.cryptomator.ui.InitializeController.InitializationListener;
import org.cryptomator.ui.UnlockController.UnlockListener;
import org.cryptomator.ui.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,11 +56,17 @@ public class MainController implements Initializable, InitializationListener, Un
@FXML
private ContextMenu directoryContextMenu;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox rootPane;
@FXML
private ListView<Directory> directoryList;
private ListView<Vault> directoryList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Pane contentPane;
@@ -67,44 +77,83 @@ public class MainController implements Initializable, InitializationListener, Un
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
final ObservableList<Vault> items = FXCollections.observableList(Settings.load().getDirectories());
directoryList.setItems(items);
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
}
@FXML
private void didClickAddDirectory(ActionEvent event) {
final DirectoryChooser dirChooser = new DirectoryChooser();
final File file = dirChooser.showDialog(stage);
if (file != null) {
addDirectory(file.toPath());
private void didClickAddVault(ActionEvent event) {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.RIGHT, 0.0, 0.0);
}
}
@FXML
private void willShowAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(true);
}
@FXML
private void didHideAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(false);
}
@FXML
private void didClickCreateNewVault(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*.cryptomator"));
final File file = fileChooser.showSaveDialog(stage);
try {
if (file != null) {
final Path vaultDir = Files.createDirectory(file.toPath());
final Path vaultShortcutFile = vaultDir.resolve(vaultDir.getFileName());
Files.createFile(vaultShortcutFile);
addVault(vaultDir, true);
}
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*.cryptomator"));
final List<File> files = fileChooser.showOpenMultipleDialog(stage);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), false);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param file non-null, writable, existing directory
* @param dir non-null, writable, existing directory
*/
void addDirectory(final Path file) {
if (file != null && Files.isWritable(file)) {
final Directory dir = new Directory(file);
if (!directoryList.getItems().contains(dir)) {
directoryList.getItems().add(dir);
void addVault(final Path dir, boolean select) {
if (dir != null && Files.isWritable(dir)) {
final Vault vault = new Vault(dir);
if (!directoryList.getItems().contains(vault)) {
directoryList.getItems().add(vault);
}
directoryList.getSelectionModel().select(dir);
directoryList.getSelectionModel().select(vault);
}
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(directoryContextMenu);
return cell;
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Vault> change) {
final Vault selectedDir = directoryList.getSelectionModel().getSelectedItem();
if (selectedDir == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
@@ -116,7 +165,7 @@ public class MainController implements Initializable, InitializationListener, Un
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
final Vault selectedDir = directoryList.getSelectionModel().getSelectedItem();
directoryList.getItems().remove(selectedDir);
directoryList.getSelectionModel().clearSelection();
}
@@ -125,7 +174,7 @@ public class MainController implements Initializable, InitializationListener, Un
// Subcontroller for right panel
// ****************************************
private void showDirectory(Directory directory) {
private void showDirectory(Vault directory) {
try {
if (directory.isUnlocked()) {
this.showUnlockedView(directory);
@@ -155,7 +204,7 @@ public class MainController implements Initializable, InitializationListener, Un
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Directory directory) {
private void showInitializeView(Vault directory) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
@@ -166,7 +215,7 @@ public class MainController implements Initializable, InitializationListener, Un
showUnlockView(ctrl.getDirectory());
}
private void showUnlockView(Directory directory) {
private void showUnlockView(Vault directory) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
@@ -178,7 +227,7 @@ public class MainController implements Initializable, InitializationListener, Un
Platform.setImplicitExit(false);
}
private void showUnlockedView(Directory directory) {
private void showUnlockedView(Vault directory) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
@@ -194,11 +243,11 @@ public class MainController implements Initializable, InitializationListener, Un
/* Convenience */
public Collection<Directory> getDirectories() {
public Collection<Vault> getDirectories() {
return directoryList.getItems();
}
public Collection<Directory> getUnlockedDirectories() {
public Collection<Vault> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}

View File

@@ -37,10 +37,9 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
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.model.Vault;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,7 +49,7 @@ public class UnlockController implements Initializable {
private ResourceBundle rb;
private UnlockListener listener;
private Directory directory;
private Vault directory;
@FXML
private ComboBox<String> usernameBox;
@@ -186,11 +185,11 @@ public class UnlockController implements Initializable {
/* Getter/Setter */
public Directory getDirectory() {
public Vault getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
public void setDirectory(Vault directory) {
this.directory = directory;
this.findExistingUsernames();
this.checkIntegrity.setSelected(directory.shouldVerifyFileIntegrity());

View File

@@ -26,7 +26,7 @@ import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.webdav.WebDavServer;
public class UnlockedController implements Initializable {
@@ -35,7 +35,7 @@ public class UnlockedController implements Initializable {
private static final double IO_SAMPLING_INTERVAL = 0.25;
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
private Vault directory;
private Timeline ioAnimation;
@FXML
@@ -118,11 +118,11 @@ public class UnlockedController implements Initializable {
/* Getter/Setter */
public Directory getDirectory() {
public Vault getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
public void setDirectory(Vault directory) {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), WebDavServer.getInstance().getPort());
messageLabel.setText(msg);

View File

@@ -8,9 +8,9 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
public class DirectoryListCell extends DraggableListCell<Directory> implements ChangeListener<Boolean> {
public class DirectoryListCell extends DraggableListCell<Vault> implements ChangeListener<Boolean> {
// fill: #FD4943, stroke: #E1443F
private static final Color RED_FILL = Color.rgb(253, 73, 67);
@@ -29,8 +29,8 @@ public class DirectoryListCell extends DraggableListCell<Directory> implements C
}
@Override
protected void updateItem(Directory item, boolean empty) {
final Directory oldItem = super.getItem();
protected void updateItem(Vault item, boolean empty) {
final Vault oldItem = super.getItem();
if (oldItem != null) {
oldItem.unlockedProperty().removeListener(this);
}

View File

@@ -14,7 +14,7 @@ import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.Main;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
@@ -27,31 +27,34 @@ import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = DirectorySerializer.class)
@JsonDeserialize(using = DirectoryDeserializer.class)
public class Directory implements Serializable {
@JsonSerialize(using = VaultSerializer.class)
@JsonDeserialize(using = VaultDeserializer.class)
public class Vault implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Runnable shutdownTask = new ShutdownTask();
private final Path path;
private boolean verifyFileIntegrity;
private String mountName = "Cryptomator";
private String mountName;
private ServletLifeCycleAdapter webDavServlet;
private WebDavMount webDavMount;
public Directory(final Path path) {
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Not a directory: " + path);
public Vault(final Path vaultDirectoryPath) {
if (!Files.isDirectory(vaultDirectoryPath) || !vaultDirectoryPath.getFileName().toString().endsWith(VAULT_FILE_EXTENSION)) {
throw new IllegalArgumentException("Not a valid vault directory: " + vaultDirectoryPath);
}
this.path = path;
this.path = vaultDirectoryPath;
try {
setMountName(getName());
} catch (IllegalArgumentException e) {
// mount name needs to be set by the user explicitly later
}
}
@@ -65,7 +68,7 @@ public class Directory implements Serializable {
}
webDavServlet = WebDavServer.getInstance().createServlet(path, verifyFileIntegrity, cryptor, getMountName());
if (webDavServlet.start()) {
MainApplication.addShutdownTask(shutdownTask);
Main.addShutdownTask(shutdownTask);
return true;
} else {
return false;
@@ -74,7 +77,7 @@ public class Directory implements Serializable {
public void stopServer() {
if (webDavServlet != null && webDavServlet.isRunning()) {
MainApplication.removeShutdownTask(shutdownTask);
Main.removeShutdownTask(shutdownTask);
this.unmount();
webDavServlet.stop();
cryptor.swipeSensitiveData();
@@ -122,14 +125,10 @@ public class Directory implements Serializable {
}
/**
* @return Directory name without preceeding path components
* @return Directory name without preceeding path components and file extension
*/
public String getName() {
String name = path.getFileName().toString();
if (StringUtils.endsWithIgnoreCase(name, Aes256Cryptor.FOLDER_EXTENSION)) {
name = name.substring(0, name.length() - Aes256Cryptor.FOLDER_EXTENSION.length());
}
return name;
return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
}
public Cryptor getCryptor() {
@@ -182,8 +181,7 @@ public class Directory implements Serializable {
* sets the mount name while normalizing it
*
* @param mountName
* @throws IllegalArgumentException
* if the name is empty after normalization
* @throws IllegalArgumentException if the name is empty after normalization
*/
public void setMountName(String mountName) throws IllegalArgumentException {
mountName = normalize(mountName);
@@ -202,8 +200,8 @@ public class Directory implements Serializable {
@Override
public boolean equals(Object obj) {
if (obj instanceof Directory) {
final Directory other = (Directory) obj;
if (obj instanceof Vault) {
final Vault other = (Vault) obj;
return this.path.equals(other.path);
} else {
return false;

View File

@@ -10,14 +10,14 @@ import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
public class DirectoryDeserializer extends JsonDeserializer<Directory> {
public class VaultDeserializer extends JsonDeserializer<Vault> {
@Override
public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
public Vault deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
final Directory dir = new Directory(path);
final Vault dir = new Vault(path);
final boolean verifyFileIntegrity = node.has("checkIntegrity") ? node.get("checkIntegrity").asBoolean() : false;
dir.setVerifyFileIntegrity(verifyFileIntegrity);
if (node.has("mountName")) {

View File

@@ -7,10 +7,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class DirectorySerializer extends JsonSerializer<Directory> {
public class VaultSerializer extends JsonSerializer<Vault> {
@Override
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeBooleanField("checkIntegrity", value.shouldVerifyFileIntegrity());

View File

@@ -21,7 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,7 +54,7 @@ public class Settings implements Serializable {
}
}
private List<Directory> directories;
private List<Vault> directories;
private Settings() {
// private constructor
@@ -95,14 +95,14 @@ public class Settings implements Serializable {
/* Getter/Setter */
public List<Directory> getDirectories() {
public List<Vault> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setDirectories(List<Directory> directories) {
public void setDirectories(List<Vault> directories) {
this.directories = directories;
}

View File

@@ -775,6 +775,13 @@ is being used to size a border should also be in pixels.
-fx-orientation: horizontal;
}
.tool-bar.list-related-toolbar {
-fx-background-color: transparent;
-fx-padding: 0.1em 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
/*******************************************************************************
* *
* Slider *

View File

@@ -206,7 +206,6 @@
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
@@ -362,6 +361,30 @@
-fx-orientation: vertical;
}
.tool-bar.list-related-toolbar {
-fx-background-color: #B4B4B4, #F7F7F7;
-fx-background-insets: 0, 0 1 1 1;
-fx-padding: 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
.tool-bar.list-related-toolbar .button,
.tool-bar.list-related-toolbar .toggle-button {
-fx-background-color: transparent;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-border-color: transparent #B4B4B4 transparent transparent;
-fx-border-width: 1;
}
.tool-bar.list-related-toolbar .button:armed,
.tool-bar.list-related-toolbar .toggle-button:armed,
.tool-bar.list-related-toolbar .toggle-button:selected {
-fx-background-color: linear-gradient(to bottom, #C0C0C0 0%, #ADADAD 100%);
}
/*******************************************************************************
* *
* ScrollBar *

View File

@@ -358,6 +358,13 @@
-fx-orientation: vertical;
}
.tool-bar.list-related-toolbar {
-fx-background-color: transparent;
-fx-padding: 0.1em 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
/*******************************************************************************
* *
* ScrollBar *

View File

@@ -13,11 +13,15 @@
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Separator?>
<?import javafx.geometry.Insets?>
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="20" right="20" bottom="20" left="20.0"/></padding>
<fx:define>
<fx:include fx:id="welcomeView" source="welcome.fxml" />
@@ -29,15 +33,21 @@
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
</items>
</ContextMenu>
<ContextMenu fx:id="addVaultContextMenu" onShowing="#willShowAddVaultContextMenu" onHidden="#didHideAddVaultContextMenu">
<items>
<MenuItem text="%main.addDirectory.contextMenu.new" onAction="#didClickCreateNewVault" />
<MenuItem text="%main.addDirectory.contextMenu.open" onAction="#didClickAddExistingVaults" />
</items>
</ContextMenu>
</fx:define>
<children>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<ToolBar VBox.vgrow="NEVER">
<ToolBar VBox.vgrow="NEVER" styleClass="list-related-toolbar">
<items>
<Button text="+" onAction="#didClickAddDirectory" />
<ToggleButton text="+" fx:id="addVaultButton" onAction="#didClickAddVault" focusTraversable="false"/>
</items>
</ToolBar>
</children>

View File

@@ -25,9 +25,9 @@
<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
<QuadCurve AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
<QuadCurve AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
</children>
</AnchorPane>

View File

@@ -13,6 +13,8 @@ app.name=Cryptomator
main.directoryList.contextMenu.remove=Remove from list
main.directoryList.contextMenu.addUser=Add user
main.directoryList.contextMenu.changePassword=Change password
main.addDirectory.contextMenu.new=Create new vault
main.addDirectory.contextMenu.open=Add existing vault
# welcome.fxml

View File

@@ -1,21 +0,0 @@
package org.cryptomator.ui.model;
import static org.junit.Assert.*;
import org.cryptomator.ui.model.Directory;
import org.junit.Test;
public class DirectoryTest {
@Test
public void testNormalize() throws Exception {
assertEquals("_", Directory.normalize(" "));
assertEquals("a", Directory.normalize("ä"));
assertEquals("C", Directory.normalize("Ĉ"));
assertEquals("_", Directory.normalize(":"));
assertEquals("", Directory.normalize("汉语"));
}
}

View File

@@ -0,0 +1,18 @@
package org.cryptomator.ui.model;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VaultTest {
@Test
public void testNormalize() throws Exception {
assertEquals("_", Vault.normalize(" "));
assertEquals("a", Vault.normalize("ä"));
assertEquals("C", Vault.normalize("Ĉ"));
assertEquals("_", Vault.normalize(":"));
assertEquals("", Vault.normalize("汉语"));
}
}