Sebastian Stenzel hace 9 años
padre
commit
b691e374eb

+ 1 - 1
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();
 	}
 

+ 132 - 36
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<Character> 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<Character> 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<? extends String> 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<Character> {
+
+		@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 <code>null</code> always on first, as this is "auto-assign".
+	 */
+	private static class WinDriveLetterComparator implements Comparator<Character> {
+
+		@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<? extends Character> 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<Boolean> futureMount = exec.submit(() -> (boolean) vault.mount());
+			final Future<Boolean> 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<? extends String> 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() {

+ 33 - 10
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<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
 
 	private String mountName;
+	private Character winDriveLetter;
 	private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
 	private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
 
@@ -108,14 +114,21 @@ public class Vault implements Serializable {
 		whitelistedResourcesWithInvalidMac.clear();
 		namesOfResourcesWithInvalidMac.clear();
 	}
+	
+	private Map<MountParam, Optional<String>> getMountParams() {
+		return ImmutableMap.of( //
+				MountParam.MOUNT_NAME, Optional.ofNullable(mountName), //
+				MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)) //
+				);
+	}
 
 	public boolean mount() {
-		Optional<ServletLifeCycleAdapter> 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<String> 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 */
 

+ 10 - 1
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<ObjectMapper> {
 		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<ObjectMapper> {
 			if (node.has("mountName")) {
 				vault.setMountName(node.get("mountName").asText());
 			}
+			if (node.has("winDriveLetter")) {
+				vault.setWinDriveLetter(CharUtils.toCharacterObject(node.get("winDriveLetter").asText()));
+			}
 			return vault;
 		}
 

+ 3 - 1
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<MountParam, Optional<String>> mountParams) {
 		displayMountInstructions();
 		return new AbstractWebDavMount() {
 			@Override

+ 43 - 41
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<MountParam, Optional<String>> 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();
-				}
+		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;
 			}
-
-			@Override
-			public void reveal() throws CommandFailedException {
-				try {
-					openMountWithWebdavUri("dav:"+uri.getRawSchemeSpecificPart()).execute();
-				} catch (CommandFailedException exception) {
-					openMountWithWebdavUri("webdav:"+uri.getRawSchemeSpecificPart()).execute();
-				}
+			// 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);
+		}
+		
 	}
 
 }

+ 34 - 23
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<MountParam, Optional<String>> 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();
+		}
+		
 	}
 
 }

+ 7 - 2
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<MountParam, Optional<String>> mountParams) throws CommandFailedException;
 
 }

+ 7 - 1
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<Character> getOccupiedDriveLetters() {
+		if (!SystemUtils.IS_OS_WINDOWS) {
+			throw new UnsupportedOperationException("This method is only defined for Windows file systems");
+		}
 		Iterable<Path> 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<Character> getAvailableDriveLetters() {

+ 55 - 42
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<MountParam, Optional<String>> 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 \"<local>;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();
-			}
-		};
-	}
-
-	private boolean isVolumeMounted(String driveLetter) {
-		for (Path path : FileSystems.getDefault().getRootDirectories()) {
-			if (path.toString().startsWith(driveLetter)) {
-				return true;
-			}
-		}
-		return false;
+		return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter);
 	}
-
+	
 	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);
+		}
+	}
 
 }

+ 26 - 0
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														*

+ 5 - 0
main/ui/src/main/resources/fxml/unlock.fxml

@@ -23,6 +23,7 @@
 <?import javafx.scene.text.Text?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.control.Separator?>
+<?import javafx.scene.control.ChoiceBox?>
 
 <GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml">
 	<padding>
@@ -69,6 +70,10 @@
 			<!-- Row 3.1 -->
 			<Label text="%unlock.label.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" />
 			<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
+			
+			<!-- Row 3.2 -->
+			<Label text="%unlock.label.winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="0" />
+			<ChoiceBox fx:id="winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
 		</GridPane>
 		
 		<!-- Row 4 -->

+ 2 - 0
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.