mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 12:09:45 +00:00
Merge pull request #2996 from cryptomator/feature/2856-folder-mounts-win
Improve handling of folder-mounts on Win
This commit is contained in:
commit
221b4e85bc
@ -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;
|
||||
}
|
||||
}
|
@ -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.<br>
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package org.cryptomator.common.mount;
|
||||
|
||||
public class MountPointNotExistsException extends IllegalMountPointException {
|
||||
|
||||
public MountPointNotExistsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> exceptionType;
|
||||
private final ObservableValue<Path> exceptionPath;
|
||||
private final ObservableValue<String> exceptionMessage;
|
||||
private final ObservableValue<Path> hideawayPath;
|
||||
private final ObservableValue<String> format;
|
||||
private final ObservableValue<Boolean> showPreferences;
|
||||
private final ObservableValue<Boolean> showVaultOptions;
|
||||
|
||||
public FormattedLabel dialogDescription;
|
||||
|
||||
@Inject
|
||||
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow AtomicReference<Throwable> unlockException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
|
||||
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow ObjectProperty<IllegalMountPointException> 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<Path> exceptionPathProperty() {
|
||||
return exceptionPath;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<String> formatProperty() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public String getExceptionMessage() {
|
||||
return exceptionMessage.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<String> exceptionMessageProperty() {
|
||||
return exceptionMessage;
|
||||
}
|
||||
|
||||
public Path getHideawayPath() {
|
||||
return hideawayPath.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Path> hideawayPathProperty() {
|
||||
return hideawayPath;
|
||||
}
|
||||
|
||||
public Boolean getShowPreferences() {
|
||||
return showPreferences.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> showPreferencesProperty() {
|
||||
return showPreferences;
|
||||
}
|
||||
|
||||
public Boolean getShowVaultOptions() {
|
||||
return showVaultOptions.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> showVaultOptionsProperty() {
|
||||
return showVaultOptions;
|
||||
}
|
||||
}
|
@ -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<Throwable> unlockException() {
|
||||
return new AtomicReference<>();
|
||||
static ObjectProperty<IllegalMountPointException> illegalMountPointException() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -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<Boolean> {
|
||||
private final Lazy<Scene> invalidMountPointScene;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||
private final AtomicReference<Throwable> unlockFailedException;
|
||||
private final ObjectProperty<IllegalMountPointException> illegalMountPointException;
|
||||
|
||||
@Inject
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow AtomicReference<Throwable> unlockFailedException) {
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vaultService = vaultService;
|
||||
@ -51,7 +51,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
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<Boolean> {
|
||||
|
||||
private void handleIllegalMountPointError(IllegalMountPointException impe) {
|
||||
Platform.runLater(() -> {
|
||||
unlockFailedException.set(impe);
|
||||
illegalMountPointException.set(impe);
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
window.show();
|
||||
});
|
||||
|
@ -40,7 +40,7 @@
|
||||
</padding>
|
||||
</Label>
|
||||
|
||||
<FormattedLabel fx:id="dialogDescription" wrapText="true" textAlignment="LEFT"/>
|
||||
<FormattedLabel fx:id="dialogDescription" wrapText="true" textAlignment="LEFT" format="${controller.format}" arg1="${controller.exceptionPath}" arg2="${controller.exceptionMessage}" arg3="${controller.hideawayPath}"/>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user