diff --git a/src/main/java/org/cryptomator/common/mount/ExistingHideawayException.java b/src/main/java/org/cryptomator/common/mount/ExistingHideawayException.java new file mode 100644 index 000000000..aa720d42c --- /dev/null +++ b/src/main/java/org/cryptomator/common/mount/ExistingHideawayException.java @@ -0,0 +1,17 @@ +package org.cryptomator.common.mount; + +import java.nio.file.Path; + +public class ExistingHideawayException extends IllegalMountPointException { + + private final Path hideaway; + + public ExistingHideawayException(Path path, Path hideaway, String msg) { + super(path, msg); + 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 6a7d96d5c..cabaf6775 100644 --- a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java +++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java @@ -5,7 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.nio.file.DirectoryNotEmptyException; +import java.io.UncheckedIOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -23,14 +23,15 @@ public final class MountWithinParentUtil { private MountWithinParentUtil() {} - static void prepareParentNoMountPoint(Path mountPoint) throws MountPointPreparationException { + static void prepareParentNoMountPoint(Path mountPoint) throws IllegalMountPointException { Path hideaway = getHideaway(mountPoint); var mpExists = removeResidualJunction(mountPoint); //Handle junction as not existing var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS); //TODO: possible improvement by just deleting an _empty_ hideaway + //TODO: Remove "ExistingHideawayException" if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist - throw new MountPointPreparationException(new NoSuchFileException(mountPoint.toString())); + throw new MountPointNotExistingException(mountPoint); } else if (!mpExists) { //only hideaway exists checkIsDirectory(hideaway); LOG.info("Mountpoint {} seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); @@ -39,7 +40,7 @@ public final class MountWithinParentUtil { Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS); } } catch (IOException e) { - throw new MountPointPreparationException(e); + throw new UncheckedIOException(e); } } else { //mountpoint exists... try { @@ -58,20 +59,21 @@ 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); + throw new UncheckedIOException(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new MountPointPreparationException(e); + throw new RuntimeException(e); } } } + //TODO Remove MountPointPreparationException private static boolean removeResidualJunction(Path path) throws MountPointPreparationException { try { if (Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isOther()) { @@ -87,6 +89,7 @@ public final class MountWithinParentUtil { } } + //TODO Remove MountPointPreparationException private static void removeResidualHideaway(Path hideaway) throws IOException { if (!Files.isDirectory(hideaway, LinkOption.NOFOLLOW_LINKS)) { throw new MountPointPreparationException(new NotDirectoryException(hideaway.toString())); @@ -130,16 +133,16 @@ public final class MountWithinParentUtil { } } - private static void checkIsDirectory(Path toCheck) throws MountPointPreparationException { + private static void checkIsDirectory(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 checkIsEmpty(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/controls/FormattedLabel.java b/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java index 04ed7e477..4c0883cba 100644 --- a/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java +++ b/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java @@ -13,18 +13,19 @@ public class FormattedLabel extends Label { private final StringProperty format = new SimpleStringProperty(""); private final ObjectProperty arg1 = new SimpleObjectProperty<>(); private final ObjectProperty arg2 = new SimpleObjectProperty<>(); - // add arg2, arg3, ... on demand + private final ObjectProperty arg3 = new SimpleObjectProperty<>(); + // add arg4, arg5, ... on demand public FormattedLabel() { textProperty().bind(createStringBinding()); } protected StringBinding createStringBinding() { - return Bindings.createStringBinding(this::updateText, format, arg1, arg2); + return Bindings.createStringBinding(this::updateText, format, arg1, arg2, arg3); } private String updateText() { - return String.format(format.get(), arg1.get(), arg2.get()); + return String.format(format.get(), arg1.get(), arg2.get(), arg3.get()); } /* Observables */ @@ -64,4 +65,16 @@ public class FormattedLabel extends Label { public void setArg2(Object arg2) { this.arg2.set(arg2); } + + public ObjectProperty arg3Property() { + return arg3; + } + + public Object getArg3() { + return arg3.get(); + } + + public void setArg3(Object arg3) { + this.arg3.set(arg3); + } } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java index 83bd80df4..6e11ed131 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -1,7 +1,11 @@ package org.cryptomator.ui.unlock; +import org.cryptomator.common.mount.ExistingHideawayException; +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,10 +13,12 @@ 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.fxml.FXML; import javafx.stage.Stage; +import java.nio.file.Path; import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicReference; @@ -25,26 +31,32 @@ public class UnlockInvalidMountPointController implements FxController { private final FxApplicationWindows appWindows; private final ResourceBundle resourceBundle; private final ExceptionType exceptionType; + private final Path exceptionPath; private final String exceptionMessage; + private final Path hideawayPath; 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 AtomicReference illegalMountPointException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) { this.window = window; this.vault = vault; this.appWindows = appWindows; this.resourceBundle = resourceBundle; - var exc = unlockException.get(); + var exc = illegalMountPointException.get(); this.exceptionType = getExceptionType(exc); + this.exceptionPath = exc.getMountpoint(); this.exceptionMessage = exc.getMessage(); + this.hideawayPath = exc instanceof ExistingHideawayException haeExc ? haeExc.getHideaway() : null; } @FXML public void initialize() { dialogDescription.setFormat(resourceBundle.getString(exceptionType.translationKey)); - dialogDescription.setArg1(exceptionMessage); + dialogDescription.setArg1(exceptionPath); + dialogDescription.setArg2(exceptionMessage); + dialogDescription.setArg3(hideawayPath); } @FXML @@ -67,8 +79,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 ExistingHideawayException x -> ExceptionType.HIDEAWAY_EXISTS; + case MountPointCleanupFailedException x -> ExceptionType.COULD_NOT_BE_CLEARED; + case MountPointNotEmptyDirectoryException x -> ExceptionType.NOT_EMPTY_DIRECTORY; default -> ExceptionType.GENERIC; }; } @@ -78,12 +93,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_EXISTS("unlock.error.customPath.description.hideawayExists", 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 +109,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; diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index b59fa272d..c84c12db1 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; @@ -61,7 +62,7 @@ abstract class UnlockModule { @Provides @UnlockWindow @UnlockScoped - static AtomicReference unlockException() { + static AtomicReference illegalMountPointException() { return new AtomicReference<>(); } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index cd4f5fcba..ea91a3651 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -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 AtomicReference 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 AtomicReference 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/i18n/strings.properties b/src/main/resources/i18n/strings.properties index ca051269a..d0d770329 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -131,7 +131,10 @@ 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.hideawayExists=The folder "%3$s", which is used to preserve folder properties set by you, has not been automatically cleaned up since your last mount to "%1$s". Please delete it manually and restore any properties to the original mount path if you set any. +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.