Merge branch 'develop' into feature/simplified-workflow-results

This commit is contained in:
Sebastian Stenzel
2022-03-21 13:56:24 +01:00
60 changed files with 1615 additions and 1001 deletions
@@ -5,6 +5,9 @@ import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import javax.inject.Inject;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@@ -27,4 +30,13 @@ class CustomDriveLetterChooser implements MountPointChooser {
public Optional<Path> chooseMountPoint(Volume caller) {
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
}
@Override
public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
//Drive already exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
}
return false;
}
}
@@ -4,6 +4,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -11,10 +12,10 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
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.Paths;
@@ -22,6 +23,9 @@ import java.util.Optional;
class CustomMountPointChooser implements MountPointChooser {
private static final String HIDEAWAY_PREFIX = ".~$";
private static final String HIDEAWAY_SUFFIX = ".tmp";
private static final String WIN_HIDDEN = "dos:hidden";
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
private final VaultSettings vaultSettings;
@@ -35,7 +39,6 @@ class CustomMountPointChooser implements MountPointChooser {
@Override
public boolean isApplicable(Volume caller) {
//Disable if useExperimentalFuse is required (Win + Fuse), but set to false
return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
}
@@ -47,49 +50,102 @@ class CustomMountPointChooser implements MountPointChooser {
@Override
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
case NONE -> {
//Requirement "NONE" doesn't make any sense here.
//No need to prepare/verify a Mountpoint without requiring one...
return switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> {
prepareParentNoMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield true;
}
case EMPTY_MOUNT_POINT -> {
prepareEmptyMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield false;
}
case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
return false;
};
}
private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
//This the case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
Path parent = mountPoint.getParent();
if (!Files.isDirectory(parent)) {
throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
}
//We must use #notExists() here because notExists =/= !exists (see docs)
if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
//File exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
//This is case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
Path hideaway = getHideaway(mountPoint);
var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
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 InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString()));
} else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString()));
} else if (!mpExists) { //only hideaway exists
checkIsDirectory(hideaway);
LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
try {
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
} else { //only mountpoint exists
try {
checkIsDirectory(mountPoint);
checkIsEmpty(mountPoint);
Files.move(mountPoint, hideaway);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
}
}
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
//This is the case for Windows when using Dokany and for Linux and Mac
if (!Files.isDirectory(mountPoint)) {
throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
if (ds.iterator().hasNext()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
}
checkIsDirectory(mountPoint);
try {
checkIsEmpty(mountPoint);
} catch (IOException exception) {
throw new InvalidMountPointException("IOException while checking folder content", exception);
}
}
@Override
public void cleanup(Volume caller, Path mountPoint) {
if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
Path hideaway = getHideaway(mountPoint);
try {
Files.move(hideaway, mountPoint);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(mountPoint, WIN_HIDDEN, false);
}
} catch (IOException e) {
LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e);
}
}
}
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
}
}
private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
try (var dirStream = Files.list(toCheck)) {
if (dirStream.findFirst().isPresent()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
}
}
}
//visible for testing
Path getHideaway(Path mountPoint) {
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
}
}
@@ -65,7 +65,7 @@ class TemporaryMountPointChooser implements MountPointChooser {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}
@@ -86,7 +86,7 @@ public class DokanyVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return MountPointRequirement.EMPTY_MOUNT_POINT;
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
@@ -4,6 +4,7 @@ import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
@@ -28,11 +29,14 @@ public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
@@ -50,7 +54,7 @@ public class FuseVolume extends AbstractVolume {
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build();
this.mount = mounter.mount(root, envVars, onExitAction);
} catch ( FuseMountException | FuseNotSupportedException e) {
} catch (FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@@ -119,7 +123,10 @@ public class FuseVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
if (!SystemUtils.IS_OS_WINDOWS) {
return MountPointRequirement.EMPTY_MOUNT_POINT;
}
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public static boolean isSupportedStatic() {
@@ -6,6 +6,11 @@ package org.cryptomator.common.vaults;
*/
public enum MountPointRequirement {
/**
* The Mountpoint needs to be a filesystem root and must not exist.
*/
UNUSED_ROOT_DIR,
/**
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
*/
@@ -35,14 +35,13 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.UUID;
@AddVaultWizardScoped
public class CreateNewVaultLocationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
private static final String TEMP_FILE_FORMAT = "cryptomator-%s.tmp";
private static final String TEMP_FILE_FORMAT = ".locationTest.cryptomator.tmp";
private final Stage window;
private final Lazy<Scene> chooseNameScene;
@@ -112,7 +111,7 @@ public class CreateNewVaultLocationController implements FxController {
}
private boolean isActuallyWritable(Path p) {
Path tmpFile = p.resolve(String.format(TEMP_FILE_FORMAT, UUID.randomUUID()));
Path tmpFile = p.resolve(TEMP_FILE_FORMAT);
try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
return true;
} catch (IOException e) {
@@ -1,5 +1,6 @@
package org.cryptomator.ui.unlock;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -32,12 +33,24 @@ public class UnlockInvalidMountPointController implements FxController {
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
}
public boolean getMustExist() {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
return requirement == MountPointRequirement.EMPTY_MOUNT_POINT;
public boolean getNotExisting() {
return getMountPointRequirement() == MountPointRequirement.EMPTY_MOUNT_POINT;
}
}
public boolean getExisting() {
return getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public boolean getDriveLetterOccupied() {
return getMountPointRequirement() == MountPointRequirement.UNUSED_ROOT_DIR;
}
private MountPointRequirement getMountPointRequirement() {
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
return requirement;
}
}
@@ -2,6 +2,7 @@ package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
@@ -79,9 +80,10 @@ public class UnlockWorkflow extends Task<Boolean> {
}
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
Throwable cause = impExc.getCause();
// TODO: apply https://openjdk.java.net/jeps/8213076 in future JDK versions
@@ -93,7 +95,11 @@ public class UnlockWorkflow extends Task<Boolean> {
}
showInvalidMountPointScene();
} else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
if (requirement == MountPointRequirement.UNUSED_ROOT_DIR) {
LOG.error("Unlock failed. Drive Letter already in use: {}", cause.getMessage());
} else {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
}
showInvalidMountPointScene();
} else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
@@ -11,9 +11,6 @@ import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
@@ -32,18 +29,15 @@ import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.Set;
/**
* TODO: if WebDav is selected on a windows system, custom mount directory is _not_ supported. This is currently not indicated/shown/etc in the ui
*/
@VaultOptionsScoped
public class MountOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final BooleanProperty osIsWindows = new SimpleBooleanProperty(SystemUtils.IS_OS_WINDOWS);
private final BooleanBinding webDavAndWindows;
private final VolumeImpl usedVolumeImpl;
private final WindowsDriveLetters windowsDriveLetters;
private final ResourceBundle resourceBundle;
public CheckBox readOnlyCheckbox;
public CheckBox customMountFlagsCheckbox;
public TextField mountFlags;
@@ -53,20 +47,13 @@ public class MountOptionsController implements FxController {
public RadioButton mountPointCustomDir;
public ChoiceBox<String> driveLetterSelection;
//FUSE + Windows -> Disable some (experimental) features for the user because they are unstable
//Use argument Dfuse.experimental="true" to override
private final BooleanBinding restrictToStableFuseOnWindows;
@Inject
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
this.window = window;
this.vault = vault;
this.webDavAndWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.WEBDAV).and(osIsWindows);
this.usedVolumeImpl = settings.preferredVolumeImpl().get();
this.windowsDriveLetters = windowsDriveLetters;
this.resourceBundle = resourceBundle;
BooleanBinding isFuseOnWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.FUSE).and(osIsWindows);
this.restrictToStableFuseOnWindows = isFuseOnWindows.and(new SimpleBooleanProperty(!environment.useExperimentalFuse())); //Is FUSE on Win and is NOT experimental fuse enabled
}
@FXML
@@ -74,10 +61,11 @@ public class MountOptionsController implements FxController {
// readonly:
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
if (getRestrictToStableFuseOnWindows()) {
//TODO: support this feature on Windows
if (usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) {
readOnlyCheckbox.setSelected(false); // to prevent invalid states
readOnlyCheckbox.setDisable(true);
}
readOnlyCheckbox.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().or(restrictToStableFuseOnWindows));
// custom mount flags:
mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
@@ -95,9 +83,7 @@ public class MountOptionsController implements FxController {
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
if (vault.getVaultSettings().useCustomMountPath().get()
&& vault.getVaultSettings().getCustomMountPath().isPresent()
&& !getRestrictToStableFuseOnWindows() /* to prevent invalid states */) {
if (vault.getVaultSettings().useCustomMountPath().get() && vault.getVaultSettings().getCustomMountPath().isPresent()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
mountPoint.selectToggle(mountPointWinDriveLetter);
@@ -188,32 +174,28 @@ public class MountOptionsController implements FxController {
// Getter & Setter
public BooleanProperty osIsWindowsProperty() {
return osIsWindows;
public boolean isOsWindows() {
return SystemUtils.IS_OS_WINDOWS;
}
public boolean getOsIsWindows() {
return osIsWindows.get();
public boolean isCustomMountPointSupported() {
return !(usedVolumeImpl == VolumeImpl.WEBDAV && isOsWindows());
}
public BooleanBinding webDavAndWindowsProperty() {
return webDavAndWindows;
}
public boolean isWebDavAndWindows() {
return webDavAndWindows.get();
public boolean isReadOnlySupported() {
return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows());
}
public StringProperty customMountPathProperty() {
return vault.getVaultSettings().customMountPath();
}
public boolean isCustomMountOptionsSupported() {
return usedVolumeImpl != VolumeImpl.WEBDAV;
}
public String getCustomMountPath() {
return vault.getVaultSettings().customMountPath().get();
}
public Boolean getRestrictToStableFuseOnWindows() {
return restrictToStableFuseOnWindows.get();
}
}
@@ -29,8 +29,9 @@
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<Text text="%unlock.error.heading" styleClass="label-large"/>
<FormattedLabel visible="${controller.mustExist}" managed="${controller.mustExist}" format="%unlock.error.invalidMountPoint.notExisting" arg1="${controller.mountPoint}" wrapText="true"/>
<FormattedLabel visible="${!controller.mustExist}" managed="${!controller.mustExist}" format="%unlock.error.invalidMountPoint.existing" arg1="${controller.mountPoint}" wrapText="true"/>
<FormattedLabel visible="${controller.notExisting}" managed="${controller.notExisting}" format="%unlock.error.invalidMountPoint.notExisting" arg1="${controller.mountPoint}" wrapText="true"/>
<FormattedLabel visible="${controller.existing}" managed="${controller.existing}" format="%unlock.error.invalidMountPoint.existing" arg1="${controller.mountPoint}" wrapText="true"/>
<FormattedLabel visible="${controller.driveLetterOccupied}" managed="${controller.driveLetterOccupied}" format="%unlock.error.invalidMountPoint.driveLetterOccupied" arg1="${controller.mountPoint}" wrapText="true"/>
</VBox>
</HBox>
@@ -24,7 +24,7 @@
<children>
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly"/>
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags" visible="${!controller.webDavAndWindows}" managed="${!controller.webDavAndWindows}"/>
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags" visible="${controller.customMountOptionsSupported}" managed="${controller.customMountOptionsSupported}"/>
<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity">
<VBox.margin>
@@ -38,19 +38,19 @@
</VBox.margin>
</Label>
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointAuto" text="%vaultOptions.mount.mountPoint.auto"/>
<HBox spacing="6" visible="${controller.osIsWindows}" managed="${controller.osIsWindows}">
<HBox spacing="6" visible="${controller.osWindows}" managed="${controller.osWindows}">
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointWinDriveLetter" text="%vaultOptions.mount.mountPoint.driveLetter"/>
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointWinDriveLetter.selected}"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT" visible="${!controller.webDavAndWindows}" managed="${!controller.webDavAndWindows}">
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom" disable="${controller.restrictToStableFuseOnWindows}"/>
<HBox fx:id="customMountPointRadioBtn" spacing="6" alignment="CENTER_LEFT" visible="${controller.customMountOptionsSupported}" managed="${controller.customMountOptionsSupported}">
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom" />
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointCustomDir.selected}">
<graphic>
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
</graphic>
</Button>
</HBox>
<TextField text="${controller.customMountPath}" visible="${mountPointCustomDir.selected}" maxWidth="Infinity" disable="true" managed="${!controller.webDavAndWindows}">
<TextField text="${controller.customMountPath}" visible="${mountPointCustomDir.selected}" maxWidth="Infinity" disable="true" managed="${customMountPointRadioBtn.managed}">
<VBox.margin>
<Insets left="24"/>
</VBox.margin>
@@ -116,6 +116,7 @@ unlock.error.heading=Unable to unlock vault
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist.
unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing.
unlock.error.invalidMountPoint.driveLetterOccupied=Drive Letter "%s" is already in use.
# Lock
## Force
+4 -4
View File
@@ -62,10 +62,10 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
GPLv2:
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
GPLv2+CE:
- javafx-base (org.openjfx:javafx-base:17.0.1 - https://openjdk.java.net/projects/openjfx/javafx-base/)
- javafx-controls (org.openjfx:javafx-controls:17.0.1 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
- javafx-fxml (org.openjfx:javafx-fxml:17.0.1 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
- javafx-graphics (org.openjfx:javafx-graphics:17.0.1 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
- javafx-base (org.openjfx:javafx-base:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-base/)
- javafx-controls (org.openjfx:javafx-controls:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
- javafx-fxml (org.openjfx:javafx-fxml:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
- javafx-graphics (org.openjfx:javafx-graphics:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
LGPL 2.1:
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
LGPL-2.1-or-later:
@@ -0,0 +1,187 @@
package org.cryptomator.common.mountpoint;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Volume;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CustomMountPointChooserTest {
//--- Mocks ---
VaultSettings vaultSettings;
Environment environment;
Volume volume;
CustomMountPointChooser customMpc;
@BeforeEach
public void init() {
this.volume = Mockito.mock(Volume.class);
this.vaultSettings = Mockito.mock(VaultSettings.class);
this.environment = Mockito.mock(Environment.class);
this.customMpc = new CustomMountPointChooser(vaultSettings, environment);
}
@Nested
public class WinfspPreperations {
@Test
@DisplayName("Hideaway name for PARENT_NO_MOUNTPOINT is not the same as mountpoint")
public void testGetHideaway() {
//prepare
Path mntPoint = Path.of("/foo/bar");
//execute
var hideaway = customMpc.getHideaway(mntPoint);
//eval
Assertions.assertNotEquals(hideaway.getFileName(), mntPoint.getFileName());
Assertions.assertEquals(hideaway.getParent(), mntPoint.getParent());
Assertions.assertTrue(hideaway.getFileName().toString().contains(mntPoint.getFileName().toString()));
}
@Test
@DisplayName("PARENT_NO_MOUNTPOINT preparations succeeds, if only mountpoint is present")
public void testPrepareParentNoMountpointOnlyMountpoint(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
Files.createDirectory(mntPoint);
//execute
Assertions.assertDoesNotThrow(() -> customMpc.prepareParentNoMountPoint(mntPoint));
//evaluate
Assertions.assertTrue(Files.notExists(mntPoint));
Path hideaway = customMpc.getHideaway(mntPoint);
Assertions.assertTrue(Files.exists(hideaway));
if(OS.WINDOWS.isCurrentOs()) {
Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden"));
}
}
@Test
@DisplayName("PARENT_NO_MOUNTPOINT preparations fail, if only non-empty mountpoint is present")
public void testPrepareParentNoMountpointOnlyNonEmptyMountpoint(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
Files.createDirectory(mntPoint);
Files.createFile(mntPoint.resolve("foo"));
//execute
Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint));
//evaluate
Assertions.assertTrue(Files.exists(mntPoint.resolve("foo")));
}
@Test
@DisplayName("PARENT_NO_MOUNTPOINT preparation succeeds, if for any reason only hideaway dir is present")
public void testPrepareParentNoMountpointOnlyHideaway(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
var hideaway = customMpc.getHideaway(mntPoint);
Files.createDirectory(hideaway); //we explicitly do not set the file attributes here
//execute
Assertions.assertDoesNotThrow(() -> customMpc.prepareParentNoMountPoint(mntPoint));
//evaluate
Assertions.assertTrue(Files.exists(hideaway));
if(OS.WINDOWS.isCurrentOs()) {
Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden"));
}
}
@Test
@DisplayName("PARENT_NO_MOUNTPOINT preparation fails, if mountpoint and hideaway dirs are present")
public void testPrepareParentNoMountpointMountPointAndHideaway(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
var hideaway = customMpc.getHideaway(mntPoint);
Files.createDirectory(hideaway); //we explicitly do not set the file attributes here
Files.createDirectory(mntPoint);
//execute
Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint));
//evaluate
Assertions.assertTrue(Files.exists(hideaway));
Assertions.assertTrue(Files.exists(mntPoint));
if(OS.WINDOWS.isCurrentOs()) {
Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden"));
}
}
@Test
@DisplayName("PARENT_NO_MOUNTPOINT preparation fails, if neither mountpoint nor hideaway dir is present")
public void testPrepareParentNoMountpointNothing(@TempDir Path tmpDir) {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
var hideaway = customMpc.getHideaway(mntPoint);
//execute
Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint));
//evaluate
Assertions.assertTrue(Files.notExists(hideaway));
Assertions.assertTrue(Files.notExists(mntPoint));
}
@Test
@DisplayName("Normal Cleanup for PARENT_NO_MOUNTPOINT")
public void testCleanupSuccess(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
var hideaway = customMpc.getHideaway(mntPoint);
Files.createDirectory(hideaway);
Mockito.when(volume.getMountPointRequirement()).thenReturn(MountPointRequirement.PARENT_NO_MOUNT_POINT);
//execute
Assertions.assertDoesNotThrow(() -> customMpc.cleanup(volume, mntPoint));
//evaluate
Assertions.assertTrue(Files.exists(mntPoint));
Assertions.assertTrue(Files.notExists(hideaway));
if(OS.WINDOWS.isCurrentOs()) {
Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden"));
}
}
@Test
@DisplayName("On IOException cleanup for PARENT_NO_MOUNTPOINT exits normally")
public void testCleanupIOFailure(@TempDir Path tmpDir) throws IOException {
//prepare
var mntPoint = tmpDir.resolve("mntPoint");
var hideaway = customMpc.getHideaway(mntPoint);
Files.createDirectory(hideaway);
Mockito.when(volume.getMountPointRequirement()).thenReturn(MountPointRequirement.PARENT_NO_MOUNT_POINT);
try (MockedStatic<Files> filesMock = Mockito.mockStatic(Files.class)) {
filesMock.when(() -> Files.move(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new IOException("error"));
//execute
Assertions.assertDoesNotThrow(() -> customMpc.cleanup(volume, mntPoint));
}
}
}
}