diff --git a/src/main/java/org/cryptomator/common/mount/HideawayNotDirectoryException.java b/src/main/java/org/cryptomator/common/mount/HideawayNotDirectoryException.java
new file mode 100644
index 000000000..506f0f10b
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/HideawayNotDirectoryException.java
@@ -0,0 +1,17 @@
+package org.cryptomator.common.mount;
+
+import java.nio.file.Path;
+
+public class HideawayNotDirectoryException extends IllegalMountPointException {
+
+ private final Path hideaway;
+
+ public HideawayNotDirectoryException(Path path, Path hideaway) {
+ super(path, "Existing hideaway (" + hideaway.toString() + ") for mountpoint is not a directory: " + path.toString());
+ this.hideaway = hideaway;
+ }
+
+ public Path getHideaway() {
+ return hideaway;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
index 5fdb1d91c..30419d85c 100644
--- a/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
+++ b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
@@ -1,9 +1,25 @@
package org.cryptomator.common.mount;
+import java.nio.file.Path;
+
+/**
+ * Indicates that validation or preparation of a mountpoint failed due to a configuration error or an invalid system state.
+ * Instances of this exception are usually caught and displayed to the user in an appropriate fashion, e.g. by {@link org.cryptomator.ui.unlock.UnlockInvalidMountPointController UnlockInvalidMountPointController.}
+ */
public class IllegalMountPointException extends IllegalArgumentException {
- public IllegalMountPointException(String msg) {
- super(msg);
+ private final Path mountpoint;
+
+ public IllegalMountPointException(Path mountpoint) {
+ this(mountpoint, "The provided mountpoint has a problem: " + mountpoint.toString());
}
-}
+ public IllegalMountPointException(Path mountpoint, String msg) {
+ super(msg);
+ this.mountpoint = mountpoint;
+ }
+
+ public Path getMountpoint() {
+ return mountpoint;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointCleanupFailedException.java b/src/main/java/org/cryptomator/common/mount/MountPointCleanupFailedException.java
new file mode 100644
index 000000000..6246e124c
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointCleanupFailedException.java
@@ -0,0 +1,10 @@
+package org.cryptomator.common.mount;
+
+import java.nio.file.Path;
+
+public class MountPointCleanupFailedException extends IllegalMountPointException {
+
+ public MountPointCleanupFailedException(Path path) {
+ super(path, "Mountpoint could not be cleared: " + path.toString());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointInUseException.java b/src/main/java/org/cryptomator/common/mount/MountPointInUseException.java
index 9f7f11174..d104cff4d 100644
--- a/src/main/java/org/cryptomator/common/mount/MountPointInUseException.java
+++ b/src/main/java/org/cryptomator/common/mount/MountPointInUseException.java
@@ -1,8 +1,10 @@
package org.cryptomator.common.mount;
+import java.nio.file.Path;
+
public class MountPointInUseException extends IllegalMountPointException {
- public MountPointInUseException(String msg) {
- super(msg);
+ public MountPointInUseException(Path path) {
+ super(path);
}
}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotEmptyDirectoryException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotEmptyDirectoryException.java
new file mode 100644
index 000000000..79801613f
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotEmptyDirectoryException.java
@@ -0,0 +1,10 @@
+package org.cryptomator.common.mount;
+
+import java.nio.file.Path;
+
+public class MountPointNotEmptyDirectoryException extends IllegalMountPointException {
+
+ public MountPointNotEmptyDirectoryException(Path path, String msg) {
+ super(path, msg);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotExistingException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotExistingException.java
new file mode 100644
index 000000000..bebf63ee3
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotExistingException.java
@@ -0,0 +1,14 @@
+package org.cryptomator.common.mount;
+
+import java.nio.file.Path;
+
+public class MountPointNotExistingException extends IllegalMountPointException {
+
+ public MountPointNotExistingException(Path path, String msg) {
+ super(path, msg);
+ }
+
+ public MountPointNotExistingException(Path path) {
+ super(path, "Mountpoint does not exist: " + path);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
deleted file mode 100644
index e90523bc2..000000000
--- a/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.cryptomator.common.mount;
-
-public class MountPointNotExistsException extends IllegalMountPointException {
-
- public MountPointNotExistsException(String msg) {
- super(msg);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
index e321f23b1..94e59121c 100644
--- a/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
@@ -1,8 +1,10 @@
package org.cryptomator.common.mount;
+import java.nio.file.Path;
+
public class MountPointNotSupportedException extends IllegalMountPointException {
- public MountPointNotSupportedException(String msg) {
- super(msg);
+ public MountPointNotSupportedException(Path path, String msg) {
+ super(path, msg);
}
}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
deleted file mode 100644
index e4734e011..000000000
--- a/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.cryptomator.common.mount;
-
-public class MountPointPreparationException extends RuntimeException {
-
- public MountPointPreparationException(String msg) {
- super(msg);
- }
-
- public MountPointPreparationException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
index 2aa9feadc..b632923a8 100644
--- a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
+++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
@@ -5,13 +5,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
public final class MountWithinParentUtil {
@@ -22,31 +20,33 @@ public final class MountWithinParentUtil {
private MountWithinParentUtil() {}
- static void prepareParentNoMountPoint(Path mountPoint) throws MountPointPreparationException {
+ static void prepareParentNoMountPoint(Path mountPoint) throws IllegalMountPointException, IOException {
Path hideaway = getHideaway(mountPoint);
- var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
+ var mpState = getMountPointState(mountPoint);
var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
- //TODO: possible improvement by just deleting an _empty_ hideaway
- if (mpExists && hideExists) { //both resources exist (whatever type)
- throw new MountPointPreparationException(new FileAlreadyExistsException(hideaway.toString()));
- } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
- throw new MountPointPreparationException(new NoSuchFileException(mountPoint.toString()));
- } else if (!mpExists) { //only hideaway exists
- checkIsDirectory(hideaway);
- LOG.info("Mountpoint {} seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
- try {
- if (SystemUtils.IS_OS_WINDOWS) {
- Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
- }
- } catch (IOException e) {
- throw new MountPointPreparationException(e);
- }
- } else { //only mountpoint exists
- try {
- checkIsDirectory(mountPoint);
- checkIsEmpty(mountPoint);
+ if (mpState == MountPointState.BROKEN_JUNCTION) {
+ LOG.info("Mountpoint \"{}\" is still a junction. Deleting it.", mountPoint);
+ Files.delete(mountPoint); //Throws if mountPoint is also a non-empty folder
+ mpState = MountPointState.NOT_EXISTING;
+ }
+ if (mpState == MountPointState.NOT_EXISTING && !hideExists) { //neither mountpoint nor hideaway exist
+ throw new MountPointNotExistingException(mountPoint);
+ } else if (mpState == MountPointState.NOT_EXISTING) { //only hideaway exists
+ checkIsHideawayDirectory(mountPoint, hideaway);
+ LOG.info("Mountpoint {} seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
+ }
+ } else {
+ assert mpState == MountPointState.EMPTY_DIR;
+ try {
+ if (hideExists) { //... with hideaway
+ removeResidualHideaway(mountPoint, hideaway);
+ }
+
+ //... (now) without hideaway
Files.move(mountPoint, hideaway);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
@@ -54,30 +54,66 @@ public final class MountWithinParentUtil {
int attempts = 0;
while (!Files.notExists(mountPoint)) {
if (attempts >= 10) {
- throw new MountPointPreparationException("Path " + mountPoint + " could not be cleared");
+ throw new MountPointCleanupFailedException(mountPoint);
}
Thread.sleep(1000);
attempts++;
}
- } catch (IOException e) {
- throw new MountPointPreparationException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- throw new MountPointPreparationException(e);
+ throw new RuntimeException(e);
}
}
}
+ //visible for testing
+ static MountPointState getMountPointState(Path path) throws IOException, IllegalMountPointException {
+ if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) {
+ return MountPointState.NOT_EXISTING;
+ }
+ if (!Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isOther()) {
+ checkIsMountPointDirectory(path);
+ checkIsMountPointEmpty(path);
+ return MountPointState.EMPTY_DIR;
+ }
+ if (Files.exists(path /* FOLLOW_LINKS */)) { //Both junction and target exist
+ throw new MountPointInUseException(path);
+ }
+ return MountPointState.BROKEN_JUNCTION;
+ }
+
+ //visible for testing
+ enum MountPointState {
+
+ NOT_EXISTING,
+
+ EMPTY_DIR,
+
+ BROKEN_JUNCTION;
+
+ }
+
+ //visible for testing
+ static void removeResidualHideaway(Path mountPoint, Path hideaway) throws IOException {
+ checkIsHideawayDirectory(mountPoint, hideaway);
+ Files.delete(hideaway); //Fails if not empty
+ }
+
static void cleanup(Path mountPoint) {
Path hideaway = getHideaway(mountPoint);
try {
waitForMountpointRestoration(mountPoint);
+ if (Files.notExists(hideaway, LinkOption.NOFOLLOW_LINKS)) {
+ LOG.error("Unable to restore hidden directory to mountpoint \"{}\": Directory does not exist.", mountPoint);
+ return;
+ }
+
Files.move(hideaway, mountPoint);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(mountPoint, WIN_HIDDEN_ATTR, false);
}
} catch (IOException e) {
- LOG.error("Unable to restore hidden directory to mountpoint {}.", mountPoint, e);
+ LOG.error("Unable to restore hidden directory to mountpoint \"{}\".", mountPoint, e);
}
}
@@ -99,16 +135,22 @@ public final class MountWithinParentUtil {
}
}
- private static void checkIsDirectory(Path toCheck) throws MountPointPreparationException {
+ private static void checkIsMountPointDirectory(Path toCheck) throws IllegalMountPointException {
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
- throw new MountPointPreparationException(new NotDirectoryException(toCheck.toString()));
+ throw new MountPointNotEmptyDirectoryException(toCheck, "Mountpoint is not a directory: " + toCheck);
}
}
- private static void checkIsEmpty(Path toCheck) throws MountPointPreparationException, IOException {
+ private static void checkIsHideawayDirectory(Path mountPoint, Path hideawayToCheck) {
+ if (!Files.isDirectory(hideawayToCheck, LinkOption.NOFOLLOW_LINKS)) {
+ throw new HideawayNotDirectoryException(mountPoint, hideawayToCheck);
+ }
+ }
+
+ private static void checkIsMountPointEmpty(Path toCheck) throws IllegalMountPointException, IOException {
try (var dirStream = Files.list(toCheck)) {
if (dirStream.findFirst().isPresent()) {
- throw new MountPointPreparationException(new DirectoryNotEmptyException(toCheck.toString()));
+ throw new MountPointNotEmptyDirectoryException(toCheck, "Mountpoint directory is not empty: " + toCheck);
}
}
}
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
index 593cb6666..101524ea3 100644
--- a/src/main/java/org/cryptomator/common/mount/Mounter.java
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -99,7 +99,7 @@ public class Mounter {
var mpIsDriveLetter = userChosenMountPoint.toString().matches("[A-Z]:\\\\");
if (mpIsDriveLetter) {
if (driveLetters.getOccupied().contains(userChosenMountPoint)) {
- throw new MountPointInUseException(userChosenMountPoint.toString());
+ throw new MountPointInUseException(userChosenMountPoint);
}
} else if (canMountToParent && !canMountToDir) {
MountWithinParentUtil.prepareParentNoMountPoint(userChosenMountPoint);
@@ -115,13 +115,13 @@ public class Mounter {
|| (!canMountToParent && !mpIsDriveLetter) //
|| (!canMountToDir && !canMountToParent && !canMountToSystem && !canMountToDriveLetter);
if (configNotSupported) {
- throw new MountPointNotSupportedException(e.getMessage());
+ throw new MountPointNotSupportedException(userChosenMountPoint, e.getMessage());
} else if (canMountToDir && !canMountToParent && !Files.exists(userChosenMountPoint)) {
//mountpoint must exist
- throw new MountPointNotExistsException(e.getMessage());
+ throw new MountPointNotExistingException(userChosenMountPoint, e.getMessage());
} else {
//TODO: add specific exception for !canMountToDir && canMountToParent && !Files.notExists(userChosenMountPoint)
- throw new IllegalMountPointException(e.getMessage());
+ throw new IllegalMountPointException(userChosenMountPoint, e.getMessage());
}
}
}
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
index 83bd80df4..53c358038 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
@@ -1,7 +1,12 @@
package org.cryptomator.ui.unlock;
+import org.cryptomator.common.ObservableUtil;
+import org.cryptomator.common.mount.HideawayNotDirectoryException;
+import org.cryptomator.common.mount.IllegalMountPointException;
+import org.cryptomator.common.mount.MountPointCleanupFailedException;
import org.cryptomator.common.mount.MountPointInUseException;
-import org.cryptomator.common.mount.MountPointNotExistsException;
+import org.cryptomator.common.mount.MountPointNotEmptyDirectoryException;
+import org.cryptomator.common.mount.MountPointNotExistingException;
import org.cryptomator.common.mount.MountPointNotSupportedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -9,12 +14,15 @@ import org.cryptomator.ui.controls.FormattedLabel;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
+import org.jetbrains.annotations.PropertyKey;
import javax.inject.Inject;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.stage.Stage;
+import java.nio.file.Path;
import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicReference;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@UnlockScoped
@@ -23,28 +31,31 @@ public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Vault vault;
private final FxApplicationWindows appWindows;
- private final ResourceBundle resourceBundle;
- private final ExceptionType exceptionType;
- private final String exceptionMessage;
+
+ private final ObservableValue exceptionType;
+ private final ObservableValue exceptionPath;
+ private final ObservableValue exceptionMessage;
+ private final ObservableValue hideawayPath;
+ private final ObservableValue format;
+ private final ObservableValue showPreferences;
+ private final ObservableValue showVaultOptions;
public FormattedLabel dialogDescription;
@Inject
- UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow AtomicReference unlockException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
+ UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow ObjectProperty illegalMountPointException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
this.appWindows = appWindows;
- this.resourceBundle = resourceBundle;
- var exc = unlockException.get();
- this.exceptionType = getExceptionType(exc);
- this.exceptionMessage = exc.getMessage();
- }
+ this.exceptionType = illegalMountPointException.map(this::getExceptionType);
+ this.exceptionPath = illegalMountPointException.map(IllegalMountPointException::getMountpoint);
+ this.exceptionMessage = illegalMountPointException.map(IllegalMountPointException::getMessage);
+ this.hideawayPath = illegalMountPointException.map(e -> e instanceof HideawayNotDirectoryException haeExc ? haeExc.getHideaway() : null);
- @FXML
- public void initialize() {
- dialogDescription.setFormat(resourceBundle.getString(exceptionType.translationKey));
- dialogDescription.setArg1(exceptionMessage);
+ this.format = ObservableUtil.mapWithDefault(exceptionType, type -> resourceBundle.getString(type.translationKey), "");
+ this.showPreferences = ObservableUtil.mapWithDefault(exceptionType, type -> type.action == ButtonAction.SHOW_PREFERENCES, false);
+ this.showVaultOptions = ObservableUtil.mapWithDefault(exceptionType, type -> type.action == ButtonAction.SHOW_VAULT_OPTIONS, false);
}
@FXML
@@ -67,8 +78,11 @@ public class UnlockInvalidMountPointController implements FxController {
private ExceptionType getExceptionType(Throwable unlockException) {
return switch (unlockException) {
case MountPointNotSupportedException x -> ExceptionType.NOT_SUPPORTED;
- case MountPointNotExistsException x -> ExceptionType.NOT_EXISTING;
+ case MountPointNotExistingException x -> ExceptionType.NOT_EXISTING;
case MountPointInUseException x -> ExceptionType.IN_USE;
+ case HideawayNotDirectoryException x -> ExceptionType.HIDEAWAY_NOT_DIR;
+ case MountPointCleanupFailedException x -> ExceptionType.COULD_NOT_BE_CLEARED;
+ case MountPointNotEmptyDirectoryException x -> ExceptionType.NOT_EMPTY_DIRECTORY;
default -> ExceptionType.GENERIC;
};
}
@@ -78,12 +92,15 @@ public class UnlockInvalidMountPointController implements FxController {
NOT_SUPPORTED("unlock.error.customPath.description.notSupported", ButtonAction.SHOW_PREFERENCES),
NOT_EXISTING("unlock.error.customPath.description.notExists", ButtonAction.SHOW_VAULT_OPTIONS),
IN_USE("unlock.error.customPath.description.inUse", ButtonAction.SHOW_VAULT_OPTIONS),
+ HIDEAWAY_NOT_DIR("unlock.error.customPath.description.hideawayNotDir", ButtonAction.SHOW_VAULT_OPTIONS),
+ COULD_NOT_BE_CLEARED("unlock.error.customPath.description.couldNotBeCleaned", ButtonAction.SHOW_VAULT_OPTIONS),
+ NOT_EMPTY_DIRECTORY("unlock.error.customPath.description.notEmptyDir", ButtonAction.SHOW_VAULT_OPTIONS),
GENERIC("unlock.error.customPath.description.generic", ButtonAction.SHOW_PREFERENCES);
private final String translationKey;
private final ButtonAction action;
- ExceptionType(String translationKey, ButtonAction action) {
+ ExceptionType(@PropertyKey(resourceBundle = "i18n.strings") String translationKey, ButtonAction action) {
this.translationKey = translationKey;
this.action = action;
}
@@ -91,6 +108,7 @@ public class UnlockInvalidMountPointController implements FxController {
private enum ButtonAction {
+ //TODO Add option to show filesystem, e.g. for ExceptionType.HIDEAWAY_EXISTS
SHOW_PREFERENCES,
SHOW_VAULT_OPTIONS;
@@ -98,11 +116,51 @@ public class UnlockInvalidMountPointController implements FxController {
/* Getter */
- public boolean isShowPreferences() {
- return exceptionType.action == ButtonAction.SHOW_PREFERENCES;
+ public Path getExceptionPath() {
+ return exceptionPath.getValue();
}
- public boolean isShowVaultOptions() {
- return exceptionType.action == ButtonAction.SHOW_VAULT_OPTIONS;
+ public ObservableValue exceptionPathProperty() {
+ return exceptionPath;
+ }
+
+ public String getFormat() {
+ return format.getValue();
+ }
+
+ public ObservableValue formatProperty() {
+ return format;
+ }
+
+ public String getExceptionMessage() {
+ return exceptionMessage.getValue();
+ }
+
+ public ObservableValue exceptionMessageProperty() {
+ return exceptionMessage;
+ }
+
+ public Path getHideawayPath() {
+ return hideawayPath.getValue();
+ }
+
+ public ObservableValue hideawayPathProperty() {
+ return hideawayPath;
+ }
+
+ public Boolean getShowPreferences() {
+ return showPreferences.getValue();
+ }
+
+ public ObservableValue showPreferencesProperty() {
+ return showPreferences;
+ }
+
+ public Boolean getShowVaultOptions() {
+ return showVaultOptions.getValue();
+ }
+
+ public ObservableValue showVaultOptionsProperty() {
+ return showVaultOptions;
}
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
index b59fa272d..f93999d21 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
@@ -4,6 +4,7 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
+import org.cryptomator.common.mount.IllegalMountPointException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
@@ -18,12 +19,13 @@ import org.jetbrains.annotations.Nullable;
import javax.inject.Named;
import javax.inject.Provider;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class UnlockModule {
@@ -61,8 +63,8 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
- static AtomicReference unlockException() {
- return new AtomicReference<>();
+ static ObjectProperty illegalMountPointException() {
+ return new SimpleObjectProperty<>();
}
@Provides
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
index cd4f5fcba..564d57ab6 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -17,11 +17,11 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
-import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -40,10 +40,10 @@ public class UnlockWorkflow extends Task {
private final Lazy invalidMountPointScene;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
- private final AtomicReference unlockFailedException;
+ private final ObjectProperty illegalMountPointException;
@Inject
- UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow AtomicReference unlockFailedException) {
+ UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty illegalMountPointException) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
@@ -51,7 +51,7 @@ public class UnlockWorkflow extends Task {
this.invalidMountPointScene = invalidMountPointScene;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
- this.unlockFailedException = unlockFailedException;
+ this.illegalMountPointException = illegalMountPointException;
}
@Override
@@ -79,7 +79,7 @@ public class UnlockWorkflow extends Task {
private void handleIllegalMountPointError(IllegalMountPointException impe) {
Platform.runLater(() -> {
- unlockFailedException.set(impe);
+ illegalMountPointException.set(impe);
window.setScene(invalidMountPointScene.get());
window.show();
});
diff --git a/src/main/resources/fxml/unlock_invalid_mount_point.fxml b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
index c6f1a31f2..dbf6fad05 100644
--- a/src/main/resources/fxml/unlock_invalid_mount_point.fxml
+++ b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
@@ -40,7 +40,7 @@
-
+
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index 137dad2c6..607717bd2 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -136,8 +136,11 @@ unlock.success.revealBtn=Reveal Drive
unlock.error.customPath.message=Unable to mount vault to custom path
unlock.error.customPath.description.notSupported=If you wish to keep using the custom path, please go to the preferences and select a volume type that supports it. Otherwise, go to the vault options and choose a supported mount point.
unlock.error.customPath.description.notExists=The custom mount path does not exist. Either create it in your local filesystem or change it in the vault options.
-unlock.error.customPath.description.inUse=Drive letter "%s" is already in use.
-unlock.error.customPath.description.generic=You have selected a custom mount path for this vault, but using it failed with the message: %s
+unlock.error.customPath.description.inUse=The drive letter or custom mount path "%s" is already in use.
+unlock.error.customPath.description.hideawayNotDir=The temporary, hidden file "%3$s" used for unlock could not be removed. Please check the file and then delete it manually.
+unlock.error.customPath.description.couldNotBeCleaned=Your vault could not be mounted to the path "%s". Please try again or choose a different path.
+unlock.error.customPath.description.notEmptyDir=The custom mount path "%s" is not an empty folder. Please choose an empty folder and try again.
+unlock.error.customPath.description.generic=You have selected a custom mount path for this vault, but using it failed with the message: %2$s
## Hub
hub.noKeychain.message=Unable to access device key
hub.noKeychain.description=In order to unlock Hub vaults, a device key is required, which is secured using a keychain. To proceed, enable ā%sā and select a keychain in the preferences.
diff --git a/src/test/java/org/cryptomator/common/mount/MountWithinParentUtilTest.java b/src/test/java/org/cryptomator/common/mount/MountWithinParentUtilTest.java
new file mode 100644
index 000000000..aad33410b
--- /dev/null
+++ b/src/test/java/org/cryptomator/common/mount/MountWithinParentUtilTest.java
@@ -0,0 +1,241 @@
+package org.cryptomator.common.mount;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static org.cryptomator.common.mount.MountWithinParentUtil.MountPointState.EMPTY_DIR;
+import static org.cryptomator.common.mount.MountWithinParentUtil.MountPointState.NOT_EXISTING;
+import static org.cryptomator.common.mount.MountWithinParentUtil.cleanup;
+import static org.cryptomator.common.mount.MountWithinParentUtil.getHideaway;
+import static org.cryptomator.common.mount.MountWithinParentUtil.getMountPointState;
+import static org.cryptomator.common.mount.MountWithinParentUtil.prepareParentNoMountPoint;
+import static org.cryptomator.common.mount.MountWithinParentUtil.removeResidualHideaway;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+class MountWithinParentUtilTest {
+
+ @Test
+ void testPrepareNeitherExist(@TempDir Path parentDir) {
+ assertThrows(MountPointNotExistingException.class, () -> {
+ prepareParentNoMountPoint(parentDir.resolve("mount"));
+ });
+ }
+
+ @Test
+ void testPrepareOnlyHideawayFileExists(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createFile(hideaway);
+
+ assertThrows(HideawayNotDirectoryException.class, () -> {
+ prepareParentNoMountPoint(mount);
+ });
+ }
+
+ @Test
+ void testPrepareOnlyHideawayDirExists(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createDirectory(hideaway);
+ assertFalse(isHidden(hideaway));
+
+ prepareParentNoMountPoint(mount);
+
+ assumeTrue(SystemUtils.IS_OS_WINDOWS);
+ assertTrue(isHidden(hideaway));
+ }
+
+ @Test
+ void testPrepareBothExistHideawayFile(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createFile(hideaway);
+ Files.createDirectory(mount);
+
+ assertThrows(HideawayNotDirectoryException.class, () -> {
+ prepareParentNoMountPoint(mount);
+ });
+ }
+
+ @Test
+ void testPrepareBothExistHideawayNotEmpty(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createDirectory(hideaway);
+ Files.createFile(hideaway.resolve("dummy"));
+ Files.createDirectory(mount);
+
+ assertThrows(DirectoryNotEmptyException.class, () -> {
+ prepareParentNoMountPoint(mount);
+ });
+ }
+
+ @Test
+ void testPrepareBothExistMountNotDir(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createFile(hideaway);
+ Files.createFile(mount);
+
+ assertThrows(MountPointNotEmptyDirectoryException.class, () -> {
+ prepareParentNoMountPoint(mount); //Must not throw something related to hideaway
+ });
+ assertTrue(Files.exists(hideaway, NOFOLLOW_LINKS));
+ }
+
+ @Test
+ void testPrepareBothExistMountNotEmpty(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createFile(hideaway);
+ Files.createDirectory(mount);
+ Files.createFile(mount.resolve("dummy"));
+
+ assertThrows(MountPointNotEmptyDirectoryException.class, () -> {
+ prepareParentNoMountPoint(mount); //Must not throw something related to hideaway
+ });
+ assertTrue(Files.exists(hideaway, NOFOLLOW_LINKS));
+ }
+
+ @Test
+ void testPrepareBothExist(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createDirectory(hideaway);
+ Files.createDirectory(mount);
+
+ prepareParentNoMountPoint(mount);
+ assertTrue(Files.notExists(mount, NOFOLLOW_LINKS));
+
+ assumeTrue(SystemUtils.IS_OS_WINDOWS);
+ assertTrue(isHidden(hideaway));
+ }
+
+ @Test
+ void testHandleMountPointFolderDoesNotExist(@TempDir Path parentDir) throws IOException {
+ assertSame(getMountPointState(parentDir.resolve("notExisting")), NOT_EXISTING);
+ }
+
+ @Test
+ void testHandleMountPointFolderIsFile(@TempDir Path parentDir) throws IOException {
+ var regularFile = parentDir.resolve("regularFile");
+ Files.createFile(regularFile);
+
+ assertThrows(MountPointNotEmptyDirectoryException.class, () -> {
+ getMountPointState(regularFile);
+ });
+ }
+
+ @Test
+ void testHandleMountPointFolderIsNotEmpty(@TempDir Path parentDir) throws IOException {
+ var regularFolder = parentDir.resolve("regularFolder");
+ var dummyFile = regularFolder.resolve("dummy");
+ Files.createDirectory(regularFolder);
+ Files.createFile(dummyFile);
+
+ assertThrows(MountPointNotEmptyDirectoryException.class, () -> {
+ getMountPointState(regularFolder);
+ });
+ }
+
+ @Test
+ void testHandleMountPointFolder(@TempDir Path parentDir) throws IOException {
+ //Sadly can't easily create files with "Other" attribute
+ var regularFolder = parentDir.resolve("regularFolder");
+ Files.createDirectory(regularFolder);
+
+ assertSame(getMountPointState(regularFolder), EMPTY_DIR);
+ }
+
+ @Test
+ void testRemoveResidualHideawayFile(@TempDir Path parentDir) throws IOException {
+ var hideaway = parentDir.resolve("hideaway");
+ Files.createFile(hideaway);
+
+ assertThrows(HideawayNotDirectoryException.class, () -> removeResidualHideaway(parentDir.resolve("mount"), hideaway));
+ }
+
+ @Test
+ void testRemoveResidualHideawayNotEmpty(@TempDir Path parentDir) throws IOException {
+ var hideaway = parentDir.resolve("hideaway");
+ Files.createDirectory(hideaway);
+ Files.createFile(hideaway.resolve("dummy"));
+
+ assertThrows(DirectoryNotEmptyException.class, () -> removeResidualHideaway(parentDir.resolve("mount"), hideaway));
+ }
+
+ @Test
+ void testCleanupNoHideaway(@TempDir Path parentDir) {
+ assertDoesNotThrow(() -> cleanup(parentDir.resolve("mount")));
+ }
+
+ @Test
+ void testCleanup(@TempDir Path parentDir) throws IOException {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+ Files.createDirectory(hideaway);
+
+ cleanup(mount);
+ assertTrue(Files.exists(mount, NOFOLLOW_LINKS));
+ assertTrue(Files.notExists(hideaway, NOFOLLOW_LINKS));
+ assertFalse(isHidden(mount));
+ }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ void testGetHideawayRootDirWin() {
+ var mount = Path.of("C:\\mount");
+ var hideaway = getHideaway(mount);
+
+ assertEquals(mount.getParent(), hideaway.getParent());
+ assertEquals(mount.getParent().resolve(".~$mount.tmp"), hideaway);
+ assertEquals(mount.getParent().toAbsolutePath() + ".~$mount.tmp", hideaway.toAbsolutePath().toString());
+ }
+
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void testGetHideawayRootDirUnix() {
+ var mount = Path.of("/mount");
+ var hideaway = getHideaway(mount);
+
+ assertEquals(mount.getParent(), hideaway.getParent());
+ assertEquals(mount.getParent().resolve(".~$mount.tmp"), hideaway);
+ assertEquals(mount.getParent().toAbsolutePath() + ".~$mount.tmp", hideaway.toAbsolutePath().toString());
+ }
+
+ @Test
+ void testGetHideaway(@TempDir Path parentDir) {
+ var mount = parentDir.resolve("mount");
+ var hideaway = getHideaway(mount);
+
+ assertEquals(mount.getParent(), hideaway.getParent());
+ assertEquals(mount.getParent().resolve(".~$mount.tmp"), hideaway);
+ assertEquals(mount.getParent().toAbsolutePath() + File.separator + ".~$mount.tmp", hideaway.toAbsolutePath().toString());
+ }
+
+ private static boolean isHidden(Path path) throws IOException {
+ try {
+ return (boolean) Objects.requireNonNullElse(Files.getAttribute(path, "dos:hidden", NOFOLLOW_LINKS), false);
+ } catch (UnsupportedOperationException | IllegalMountPointException e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file