diff --git a/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java b/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java deleted file mode 100644 index f6e476b18..000000000 --- a/main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java +++ /dev/null @@ -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 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); - } - -} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java index 131e2d57a..d7b594d64 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java @@ -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"; } diff --git a/main/ui/pom.xml b/main/ui/pom.xml index a6dd4c297..9200379f4 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -50,7 +50,7 @@ commons-lang3 - + 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 694986dce..f4618e0a5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java @@ -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 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 result = alert.showAndWait(); - return ButtonType.OK.equals(result.get()); - } - - 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) { - 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; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/Main.java b/main/ui/src/main/java/org/cryptomator/ui/Main.java index d500502ca..fca076b5a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/Main.java +++ b/main/ui/src/main/java/org/cryptomator/ui/Main.java @@ -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> OPEN_FILE_HANDLER = new CompletableFuture<>(); + private static final Set 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 = 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 ff = (List) getFiles.invoke(e); + for (File f : ff) { + handleOpenFileRequest(f); + } + } catch (RuntimeException ee) { + throw ee; + } catch (Exception ee) { + throw new RuntimeException(ee); + } + } + return null; + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 7bbf18e04..5ac1229d4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -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 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(); - } - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/MainController.java index 8bd720337..055fedc8f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainController.java @@ -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 directoryList; + private ListView 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 items = FXCollections.observableList(Settings.load().getDirectories()); + final ObservableList 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 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 createDirecoryListCell(ListView param) { + private ListCell createDirecoryListCell(ListView param) { final DirectoryListCell cell = new DirectoryListCell(); cell.setContextMenu(directoryContextMenu); return cell; } - private void selectedDirectoryDidChange(ListChangeListener.Change change) { - final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem(); + private void selectedDirectoryDidChange(ListChangeListener.Change 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 getDirectories() { + public Collection getDirectories() { return directoryList.getItems(); } - public Collection getUnlockedDirectories() { + public Collection getUnlockedDirectories() { return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet()); } 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 f4acfc28b..7054a4d44 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java @@ -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 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()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java index 4166bfbf8..b562cc77e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java @@ -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); 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 8b2f760af..6b1426958 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 @@ -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 implements ChangeListener { +public class DirectoryListCell extends DraggableListCell implements ChangeListener { // fill: #FD4943, stroke: #E1443F private static final Color RED_FILL = Color.rgb(253, 73, 67); @@ -29,8 +29,8 @@ public class DirectoryListCell extends DraggableListCell 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); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java similarity index 81% rename from main/ui/src/main/java/org/cryptomator/ui/model/Directory.java rename to main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 513d4df6c..89d08ef54 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -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 unlocked = new SimpleObjectProperty(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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultDeserializer.java similarity index 78% rename from main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java rename to main/ui/src/main/java/org/cryptomator/ui/model/VaultDeserializer.java index 1d69ec997..a9134d506 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultDeserializer.java @@ -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 { +public class VaultDeserializer extends JsonDeserializer { @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")) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultSerializer.java similarity index 73% rename from main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java rename to main/ui/src/main/java/org/cryptomator/ui/model/VaultSerializer.java index e641a4928..f413d48ff 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultSerializer.java @@ -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 { +public class VaultSerializer extends JsonSerializer { @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()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 61831295b..fd5e5b845 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -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 directories; + private List directories; private Settings() { // private constructor @@ -95,14 +95,14 @@ public class Settings implements Serializable { /* Getter/Setter */ - public List getDirectories() { + public List getDirectories() { if (directories == null) { directories = new ArrayList<>(); } return directories; } - public void setDirectories(List directories) { + public void setDirectories(List directories) { this.directories = directories; } diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index 3df5fb42d..c407c0980 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -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 * diff --git a/main/ui/src/main/resources/css/mac_theme.css b/main/ui/src/main/resources/css/mac_theme.css index 68ae9fd20..040ef3b6d 100644 --- a/main/ui/src/main/resources/css/mac_theme.css +++ b/main/ui/src/main/resources/css/mac_theme.css @@ -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 * diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index 798eb9f9e..492b653cd 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -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 * diff --git a/main/ui/src/main/resources/fxml/main.fxml b/main/ui/src/main/resources/fxml/main.fxml index 6921629f5..a3a228783 100644 --- a/main/ui/src/main/resources/fxml/main.fxml +++ b/main/ui/src/main/resources/fxml/main.fxml @@ -13,11 +13,15 @@ - + + + - + + + @@ -29,15 +33,21 @@ + + + + + + - + -