From b691e374eb2dad0284e13927e7c3fc1fdccae9bf Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 15 Oct 2015 17:14:02 +0200 Subject: [PATCH] fixes #74 --- .../org/cryptomator/ui/CryptomatorModule.java | 2 +- .../ui/controllers/UnlockController.java | 168 ++++++++++++++---- .../java/org/cryptomator/ui/model/Vault.java | 43 +++-- .../ui/model/VaultObjectMapperProvider.java | 11 +- .../ui/util/mount/FallbackWebDavMounter.java | 4 +- .../ui/util/mount/LinuxGvfsWebDavMounter.java | 86 ++++----- .../ui/util/mount/MacOsXWebDavMounter.java | 57 +++--- .../ui/util/mount/WebDavMounter.java | 9 +- .../ui/util/mount/WindowsDriveLetters.java | 8 +- .../ui/util/mount/WindowsWebDavMounter.java | 97 +++++----- main/ui/src/main/resources/css/win_theme.css | 26 +++ main/ui/src/main/resources/fxml/unlock.fxml | 5 + .../main/resources/localization.properties | 2 + 13 files changed, 359 insertions(+), 159 deletions(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index b0e0675a5..02c7075d0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -77,7 +77,7 @@ class CryptomatorModule { @Provides @Singleton - WebDavMounter provideWebDavMounterProvider(WebDavMounterProvider webDavMounterProvider) { + WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) { return webDavMounterProvider.get(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 937e4688b..424a92695 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -15,14 +15,32 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.util.Comparator; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.text.Text; +import javafx.util.StringConverter; + import javax.inject.Inject; import javax.security.auth.DestroyFailedException; import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; @@ -30,22 +48,10 @@ import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.util.FXThreads; import org.cryptomator.ui.util.mount.CommandFailedException; +import org.cryptomator.ui.util.mount.WindowsDriveLetters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.control.TextField; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.GridPane; -import javafx.scene.text.Text; - public class UnlockController extends AbstractFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); @@ -58,6 +64,9 @@ public class UnlockController extends AbstractFXMLViewController { @FXML private TextField mountName; + + @FXML + private ChoiceBox winDriveLetter; @FXML private Button advancedOptionsButton; @@ -79,11 +88,14 @@ public class UnlockController extends AbstractFXMLViewController { private final ExecutorService exec; private final Application app; + private final WindowsDriveLetters driveLetters; + private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; @Inject - public UnlockController(Application app, ExecutorService exec) { + public UnlockController(Application app, ExecutorService exec, WindowsDriveLetters driveLetters) { this.app = app; this.exec = exec; + this.driveLetters = driveLetters; } @Override @@ -99,17 +111,31 @@ public class UnlockController extends AbstractFXMLViewController { @Override public void initialize() { passwordField.textProperty().addListener(this::passwordFieldsDidChange); + advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); - advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); + if (SystemUtils.IS_OS_WINDOWS) { + winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); + } else { + winDriveLetter.setVisible(false); + winDriveLetter.setManaged(false); + } } private void resetView() { + passwordField.clear(); unlockButton.setDisable(true); advancedOptions.setVisible(false); advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show")); progressIndicator.setVisible(false); - passwordField.clear(); + if (SystemUtils.IS_OS_WINDOWS) { + winDriveLetter.valueProperty().removeListener(driveLetterChangeListener); + winDriveLetter.getItems().clear(); + winDriveLetter.getItems().add(null); + winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters()); + winDriveLetter.getItems().sort(new WinDriveLetterComparator()); + winDriveLetter.valueProperty().addListener(driveLetterChangeListener); + } downloadsPageLink.setVisible(false); messageText.setText(null); } @@ -145,6 +171,77 @@ public class UnlockController extends AbstractFXMLViewController { advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show")); } } + + private void filterAlphanumericKeyEvents(KeyEvent t) { + if (t.getCharacter() == null || t.getCharacter().length() == 0) { + return; + } + char c = CharUtils.toChar(t.getCharacter()); + if (!(CharUtils.isAsciiAlphanumeric(c) || c == '_')) { + t.consume(); + } + } + + private void mountNameDidChange(ObservableValue property, String oldValue, String newValue) { + if (vault == null) { + return; + } + // newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents + if (newValue.isEmpty()) { + mountName.setText(vault.getMountName()); + } else { + vault.setMountName(newValue); + } + } + + /** + * Converts 'C' to "C:" to translate between model and GUI. + */ + private class WinDriveLetterLabelConverter extends StringConverter { + + @Override + public String toString(Character letter) { + if (letter == null) { + return resourceBundle.getString("unlock.choicebox.winDriveLetter.auto"); + } else { + return Character.toString(letter) + ":"; + } + } + + @Override + public Character fromString(String string) { + if (resourceBundle.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) { + return null; + } else { + return CharUtils.toCharacterObject(string); + } + } + + } + + /** + * Natural sorting of ASCII letters, but null always on first, as this is "auto-assign". + */ + private static class WinDriveLetterComparator implements Comparator { + + @Override + public int compare(Character c1, Character c2) { + if (c1 == null) { + return -1; + } else if (c2 == null) { + return 1; + } else { + return (char) c1 - (char) c2; + } + } + } + + private void winDriveLetterDidChange(ObservableValue property, Character oldValue, Character newValue) { + if (vault == null) { + return; + } + vault.setWinDriveLetter(newValue); + } // **************************************** // Unlock button @@ -168,7 +265,7 @@ public class UnlockController extends AbstractFXMLViewController { // at this point we know for sure, that the masterkey can be decrypted, so lets make a backup: Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING); vault.setUnlocked(true); - final Future futureMount = exec.submit(() -> (boolean) vault.mount()); + final Future futureMount = exec.submit(vault::mount); FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished); } catch (IOException ex) { setControlsDisabled(false); @@ -228,25 +325,6 @@ public class UnlockController extends AbstractFXMLViewController { } } - public void filterAlphanumericKeyEvents(KeyEvent t) { - if (t.getCharacter() == null || t.getCharacter().length() == 0) { - return; - } - char c = t.getCharacter().charAt(0); - if (!CharUtils.isAsciiAlphanumeric(c)) { - t.consume(); - } - } - - private void mountNameDidChange(ObservableValue property, String oldValue, String newValue) { - // newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents - if (newValue.isEmpty()) { - mountName.setText(vault.getMountName()); - } else { - vault.setMountName(newValue); - } - } - /* Getter/Setter */ public Vault getVault() { @@ -257,6 +335,24 @@ public class UnlockController extends AbstractFXMLViewController { this.resetView(); this.vault = vault; this.mountName.setText(vault.getMountName()); + if (SystemUtils.IS_OS_WINDOWS) { + chooseSelectedDriveLetter(); + } + } + + private void chooseSelectedDriveLetter() { + assert SystemUtils.IS_OS_WINDOWS; + // if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this: + if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { + vault.setWinDriveLetter(null); + } + final Character letter = vault.getWinDriveLetter(); + if (letter == null) { + // first option is known to be 'auto-assign' due to #WinDriveLetterComparator. + this.winDriveLetter.getSelectionModel().selectFirst(); + } else { + this.winDriveLetter.getSelectionModel().select(letter); + } } public UnlockListener getListener() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 63d8de49c..8d0196c1a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -7,11 +7,18 @@ import java.nio.file.Path; import java.text.Normalizer; import java.text.Normalizer.Form; import java.util.HashSet; +import java.util.Map; import java.util.Optional; import java.util.Set; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import javax.security.auth.DestroyFailedException; +import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.Cryptor; import org.cryptomator.ui.util.DeferredClosable; @@ -20,15 +27,13 @@ import org.cryptomator.ui.util.FXThreads; import org.cryptomator.ui.util.mount.CommandFailedException; import org.cryptomator.ui.util.mount.WebDavMount; import org.cryptomator.ui.util.mount.WebDavMounter; +import org.cryptomator.ui.util.mount.WebDavMounter.MountParam; import org.cryptomator.webdav.WebDavServer; import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; +import com.google.common.collect.ImmutableMap; public class Vault implements Serializable { @@ -49,6 +54,7 @@ public class Vault implements Serializable { private final Set whitelistedResourcesWithInvalidMac = new HashSet<>(); private String mountName; + private Character winDriveLetter; private DeferredClosable webDavServlet = DeferredClosable.empty(); private DeferredClosable webDavMount = DeferredClosable.empty(); @@ -108,14 +114,21 @@ public class Vault implements Serializable { whitelistedResourcesWithInvalidMac.clear(); namesOfResourcesWithInvalidMac.clear(); } + + private Map> getMountParams() { + return ImmutableMap.of( // + MountParam.MOUNT_NAME, Optional.ofNullable(mountName), // + MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)) // + ); + } public boolean mount() { - Optional o = webDavServlet.get(); - if (!o.isPresent() || !o.get().isRunning()) { + final ServletLifeCycleAdapter servlet = webDavServlet.get().orElse(null); + if (servlet == null || !servlet.isRunning()) { return false; } try { - webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName)); + webDavMount = closer.closeLater(mounter.mount(servlet.getServletUri(), getMountParams())); return true; } catch (CommandFailedException e) { LOG.warn("mount failed", e); @@ -167,9 +180,7 @@ public class Vault implements Serializable { this.unlocked.set(unlocked); } - public String getMountName() { - return mountName; - } + public ObservableList getNamesOfResourcesWithInvalidMac() { return namesOfResourcesWithInvalidMac; @@ -204,6 +215,10 @@ public class Vault implements Serializable { } return builder.toString(); } + + public String getMountName() { + return mountName; + } /** * sets the mount name while normalizing it @@ -218,6 +233,14 @@ public class Vault implements Serializable { } this.mountName = mountName; } + + public Character getWinDriveLetter() { + return winDriveLetter; + } + + public void setWinDriveLetter(Character winDriveLetter) { + this.winDriveLetter = winDriveLetter; + } /* hashcode/equals */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java index 7b5932752..8dee1c905 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java @@ -8,6 +8,8 @@ import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; +import org.apache.commons.lang3.CharUtils; + import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -45,7 +47,11 @@ public class VaultObjectMapperProvider implements Provider { public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeStartObject(); jgen.writeStringField("path", value.getPath().toString()); - jgen.writeStringField("mountName", value.getMountName().toString()); + jgen.writeStringField("mountName", value.getMountName()); + final Character winDriveLetter = value.getWinDriveLetter(); + if (winDriveLetter != null) { + jgen.writeStringField("winDriveLetter", Character.toString(winDriveLetter)); + } jgen.writeEndObject(); } @@ -62,6 +68,9 @@ public class VaultObjectMapperProvider implements Provider { if (node.has("mountName")) { vault.setMountName(node.get("mountName").asText()); } + if (node.has("winDriveLetter")) { + vault.setWinDriveLetter(CharUtils.toCharacterObject(node.get("winDriveLetter").asText())); + } return vault; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java index 9029f5dbe..21b8bc454 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java @@ -9,6 +9,8 @@ package org.cryptomator.ui.util.mount; import java.net.URI; +import java.util.Map; +import java.util.Optional; /** * A WebDavMounter acting as fallback if no other mounter works. @@ -28,7 +30,7 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(URI uri, String name) { + public WebDavMount mount(URI uri, Map> mountParams) { displayMountInstructions(); return new AbstractWebDavMount() { @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java index 0a31a3a01..ed7aa3f96 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java @@ -11,6 +11,8 @@ package org.cryptomator.ui.util.mount; import java.net.URI; +import java.util.Map; +import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; @@ -22,9 +24,7 @@ import org.cryptomator.ui.util.command.Script; final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { @Inject - LinuxGvfsWebDavMounter() { - - } + LinuxGvfsWebDavMounter() {} @Override public boolean shouldWork() { @@ -47,52 +47,54 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(URI uri, String name) throws CommandFailedException { + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { final Script mountScript = Script.fromLines( "set -x", "gvfs-mount \"dav:$DAV_SSP\"") .addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); - final Script testMountStillExistsScript = Script.fromLines( - "set -x", - "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1") - .addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); - final Script unmountScript = Script.fromLines( - "set -x", - "gvfs-mount -u \"dav:$DAV_SSP\"") - .addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); mountScript.execute(); - return new AbstractWebDavMount() { - @Override - public void unmount() throws CommandFailedException { - boolean mountStillExists; - try { - testMountStillExistsScript.execute(); - mountStillExists = true; - } catch(CommandFailedException e) { - mountStillExists = false; - } - // only attempt unmount if user didn't unmount manually: - if (mountStillExists) { - unmountScript.execute(); - } - } - - @Override - public void reveal() throws CommandFailedException { - try { - openMountWithWebdavUri("dav:"+uri.getRawSchemeSpecificPart()).execute(); - } catch (CommandFailedException exception) { - openMountWithWebdavUri("webdav:"+uri.getRawSchemeSpecificPart()).execute(); - } - } - }; + return new LinuxGvfsWebDavMount(uri); } + + private static class LinuxGvfsWebDavMount extends AbstractWebDavMount { + private final URI webDavUri; + private final Script testMountStillExistsScript; + private final Script unmountScript; + + private LinuxGvfsWebDavMount(URI webDavUri) { + this.webDavUri = webDavUri; + this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + } + + @Override + public void unmount() throws CommandFailedException { + boolean mountStillExists; + try { + testMountStillExistsScript.execute(); + mountStillExists = true; + } catch(CommandFailedException e) { + mountStillExists = false; + } + // only attempt unmount if user didn't unmount manually: + if (mountStillExists) { + unmountScript.execute(); + } + } - private Script openMountWithWebdavUri(String webdavUri){ - return Script.fromLines( - "set -x", - "xdg-open \"$DAV_URI\"") - .addEnv("DAV_URI", webdavUri); + @Override + public void reveal() throws CommandFailedException { + try { + openMountWithWebdavUri("dav:"+webDavUri.getRawSchemeSpecificPart()).execute(); + } catch (CommandFailedException exception) { + openMountWithWebdavUri("webdav:"+webDavUri.getRawSchemeSpecificPart()).execute(); + } + } + + private Script openMountWithWebdavUri(String webdavUri){ + return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri); + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java index da793d5b7..22b33ec59 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java @@ -12,6 +12,8 @@ package org.cryptomator.ui.util.mount; import java.net.URI; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.util.Map; +import java.util.Optional; import java.util.UUID; import javax.inject.Inject; @@ -24,9 +26,7 @@ import org.cryptomator.ui.util.command.Script; final class MacOsXWebDavMounter implements WebDavMounterStrategy { @Inject - MacOsXWebDavMounter() { - - } + MacOsXWebDavMounter() {} @Override public boolean shouldWork() { @@ -39,7 +39,11 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(URI uri, String name) throws CommandFailedException { + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final String mountName = mountParams.get(MountParam.MOUNT_NAME).orElseThrow(() -> { + return new IllegalArgumentException("Missing mount parameter MOUNT_NAME."); + }); + // we don't use the uri to derive a path, as it *could* be longer than 255 chars. final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString(); final Script mountScript = Script.fromLines( @@ -48,28 +52,35 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy { .addEnv("DAV_AUTHORITY", uri.getRawAuthority()) .addEnv("DAV_PATH", uri.getRawPath()) .addEnv("MOUNT_PATH", path) - .addEnv("MOUNT_NAME", name); - final Script revealScript = Script.fromLines( - "open \"$MOUNT_PATH\"") - .addEnv("MOUNT_PATH", path); - final Script unmountScript = Script.fromLines( - "diskutil umount $MOUNT_PATH") - .addEnv("MOUNT_PATH", path); + .addEnv("MOUNT_NAME", mountName); mountScript.execute(); - return new AbstractWebDavMount() { - @Override - public void unmount() throws CommandFailedException { - // only attempt unmount if user didn't unmount manually: - if (Files.exists(FileSystems.getDefault().getPath(path))) { - unmountScript.execute(); - } + return new MacWebDavMount(path); + } + + private static class MacWebDavMount extends AbstractWebDavMount { + private final String mountPath; + private final Script revealScript; + private final Script unmountScript; + + private MacWebDavMount(String mountPath) { + this.mountPath = mountPath; + this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath); + this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath); + } + + @Override + public void unmount() throws CommandFailedException { + // only attempt unmount if user didn't unmount manually: + if (Files.exists(FileSystems.getDefault().getPath(mountPath))) { + unmountScript.execute(); } + } - @Override - public void reveal() throws CommandFailedException { - revealScript.execute(); - } - }; + @Override + public void reveal() throws CommandFailedException { + revealScript.execute(); + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java index 7c7a5b2b6..ce58ed6e8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java @@ -10,17 +10,22 @@ package org.cryptomator.ui.util.mount; import java.net.URI; +import java.util.Map; +import java.util.Optional; public interface WebDavMounter { + + public static enum MountParam {MOUNT_NAME, WIN_DRIVE_LETTER} /** * Tries to mount a given webdav share. * * @param uri URI of the webdav share - * @param name the name under which the folder is to be mounted. This might be ignored. + * @param mountParams additional mount parameters, that might not get ignored by some OS-specific mounters. * @return a {@link WebDavMount} representing the mounted share * @throws CommandFailedException if the mount operation fails + * @throws IllegalArgumentException if mountParams is missing expected options */ - WebDavMount mount(URI uri, String name) throws CommandFailedException; + WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsDriveLetters.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsDriveLetters.java index ef4447ab6..03b01e446 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsDriveLetters.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsDriveLetters.java @@ -11,6 +11,9 @@ import java.util.stream.StreamSupport; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.SystemUtils; + import com.google.common.collect.Sets; @@ -24,8 +27,11 @@ public final class WindowsDriveLetters { } public Set getOccupiedDriveLetters() { + if (!SystemUtils.IS_OS_WINDOWS) { + throw new UnsupportedOperationException("This method is only defined for Windows file systems"); + } Iterable rootDirs = FileSystems.getDefault().getRootDirectories(); - return StreamSupport.stream(rootDirs.spliterator(), false).map(path -> path.toString().toUpperCase().charAt(0)).collect(toSet()); + return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet()); } public Set getAvailableDriveLetters() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java index 941309b2d..e30ca707d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java @@ -12,8 +12,8 @@ package org.cryptomator.ui.util.mount; import static org.cryptomator.ui.util.command.Script.fromLines; import java.net.URI; -import java.nio.file.FileSystems; -import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -21,6 +21,7 @@ import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.util.command.CommandResult; import org.cryptomator.ui.util.command.Script; @@ -31,10 +32,11 @@ import org.cryptomator.ui.util.command.Script; * Tested on Windows 7 but should also work on Windows 8. */ @Singleton -public final class WindowsWebDavMounter implements WebDavMounterStrategy { +final class WindowsWebDavMounter implements WebDavMounterStrategy { - private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*"); + private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*"); private static final int MAX_MOUNT_ATTEMPTS = 8; + private static final char AUTO_ASSIGN_DRIVE_LETTER = '*'; private final WindowsDriveLetters driveLetters; @Inject @@ -53,50 +55,32 @@ public final class WindowsWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(URI uri, String name) throws CommandFailedException { + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final Character driveLetter = mountParams.get(MountParam.WIN_DRIVE_LETTER).map(CharUtils::toCharacterObject).orElse(AUTO_ASSIGN_DRIVE_LETTER); + if (driveLetters.getOccupiedDriveLetters().contains(driveLetter)) { + throw new CommandFailedException("Drive letter occupied."); + } + + final String driveLetterStr = driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? CharUtils.toString(AUTO_ASSIGN_DRIVE_LETTER) : driveLetter + ":"; + final Script localhostMountScript = fromLines("net use %DRIVE_LETTER% \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); + localhostMountScript.addEnv("DRIVE_LETTER", driveLetterStr); + localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); + localhostMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); CommandResult mountResult; try { - final Script mountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); - mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); - mountResult = mountScript.execute(5, TimeUnit.SECONDS); + mountResult = localhostMountScript.execute(5, TimeUnit.SECONDS); } catch (CommandFailedException ex) { - final Script localhostMountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); - localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); - final Script ipv6literaltMountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); - ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); + final Script ipv6literaltMountScript = fromLines("net use %DRIVE_LETTER% \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); + ipv6literaltMountScript.addEnv("DRIVE_LETTER", driveLetterStr); + ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); + ipv6literaltMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \";0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f"); proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript); } - - final String driveLetter = getDriveLetter(mountResult.getStdOut()); - final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter); - final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter); - return new AbstractWebDavMount() { - @Override - public void unmount() throws CommandFailedException { - // only attempt unmount if user didn't unmount manually: - if (isVolumeMounted(driveLetter)) { - unmountScript.execute(); - } - } - - @Override - public void reveal() throws CommandFailedException { - openExplorerScript.execute(); - } - }; + return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter); } - - private boolean isVolumeMounted(String driveLetter) { - for (Path path : FileSystems.getDefault().getRootDirectories()) { - if (path.toString().startsWith(driveLetter)) { - return true; - } - } - return false; - } - + private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException { CommandFailedException latestException = null; for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) { @@ -117,13 +101,42 @@ public final class WindowsWebDavMounter implements WebDavMounterStrategy { throw latestException; } - private String getDriveLetter(String result) throws CommandFailedException { + private Character getDriveLetter(String result) throws CommandFailedException { final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result); if (matcher.find()) { - return matcher.group(1); + return CharUtils.toCharacterObject(matcher.group(1)); } else { throw new CommandFailedException("Failed to get a drive letter from net use output."); } } + + private class WindowsWebDavMount extends AbstractWebDavMount { + private final Character driveLetter; + private final Script openExplorerScript; + private final Script unmountScript; + + private WindowsWebDavMount(Character driveLetter) { + this.driveLetter = driveLetter; + this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":"); + this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter)); + } + + @Override + public void unmount() throws CommandFailedException { + // only attempt unmount if user didn't unmount manually: + if (isVolumeMounted()) { + unmountScript.execute(); + } + } + + @Override + public void reveal() throws CommandFailedException { + openExplorerScript.execute(); + } + + private boolean isVolumeMounted() { + return driveLetters.getOccupiedDriveLetters().contains(driveLetter); + } + } } diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index f0bbcf280..1cc974216 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -325,6 +325,32 @@ -fx-background-color: black; } +/******************************************************************************* + * * + * ChoiceBox * + * * + ******************************************************************************/ + +.choice-box { + -fx-background-color: COLOR_BORDER, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%); + -fx-background-insets: 0, 1; + -fx-background-radius: 0, 0; + -fx-padding: 0.1em 0.6em 0.1em 0.6em; + -fx-text-fill: COLOR_TEXT; +} + +.choice-box > .open-button > .arrow { + -fx-background-color: transparent, COLOR_TEXT; + -fx-background-insets: 0 0 -1 0, 0; + -fx-padding: 0.166667em 0.333333em 0.166667em 0.333333em; /* 2 4 2 4 */ + -fx-shape: "M 0 0 h 7 l -3.5 4 z"; +} + +.choice-box .context-menu { + -fx-background-color: COLOR_BORDER, #FFF; + -fx-background-insets: 0, 1; +} + /**************************************************************************** * * * ProgressIndicator * diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 05d2cc15c..f11fdd991 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -23,6 +23,7 @@ + @@ -69,6 +70,10 @@ diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties index d476751a4..79032e80b 100644 --- a/main/ui/src/main/resources/localization.properties +++ b/main/ui/src/main/resources/localization.properties @@ -30,11 +30,13 @@ initialize.button.ok=Create vault # unlock.fxml unlock.label.password=Password unlock.label.mountName=Drive name +unlock.label.winDriveLetter=Drive letter unlock.label.downloadsPageLink=All Cryptomator versions unlock.label.advancedHeading=Advanced options unlock.button.unlock=Unlock vault unlock.button.advancedOptions.show=More options unlock.button.advancedOptions.hide=Less options +unlock.choicebox.winDriveLetter.auto=Assign automatically unlock.errorMessage.wrongPassword=Wrong password. unlock.errorMessage.decryptionFailed=Decryption failed. unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.