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