From 55d1a8e9352b745a8d01c912bd2037253a8b1a86 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Fri, 25 Feb 2022 13:21:45 +0100 Subject: [PATCH 1/8] Allow custom mount point for winfsp --- .../mountpoint/CustomMountPointChooser.java | 58 ++++++++++++++----- .../vaultoptions/MountOptionsController.java | 46 +++++---------- .../resources/fxml/vault_options_mount.fxml | 4 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index c55ede640..d075e1292 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -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; @@ -12,9 +13,7 @@ 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.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; @@ -35,7 +34,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(); } @@ -48,8 +46,16 @@ 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 PARENT_NO_MOUNT_POINT -> { + prepareParentNoMountPoint(mountPoint); + LOG.debug("Successfully checked custom mount point: {}", mountPoint); + return true; + } + case EMPTY_MOUNT_POINT -> { + prepareEmptyMountPoint(mountPoint); + LOG.debug("Successfully checked custom mount point: {}", mountPoint); + return false; + } case NONE -> { //Requirement "NONE" doesn't make any sense here. //No need to prepare/verify a Mountpoint without requiring one... @@ -60,21 +66,26 @@ class CustomMountPointChooser implements MountPointChooser { 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())); + assert SystemUtils.IS_OS_WINDOWS; + + Path hideaway = getHideaway(mountPoint); + if (Files.exists(hideaway)) { + LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount."); + } else if (!Files.isDirectory(mountPoint)) { + throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); //simulate we need a directory + } else { + //TODO: should we require it to be empty? + try { + Files.move(mountPoint, hideaway); + Files.setAttribute(hideaway, "dos:hidden", true); + } catch (IOException e) { + throw new InvalidMountPointException(e); + } } } @@ -92,4 +103,21 @@ class CustomMountPointChooser implements MountPointChooser { } } + @Override + public void cleanup(Volume caller, Path mountPoint) { + if (VolumeImpl.FUSE == caller.getImplementationType() && MountPointRequirement.PARENT_NO_MOUNT_POINT == caller.getMountPointRequirement()) { + Path hideaway = getHideaway(mountPoint); + try { + Files.move(hideaway, mountPoint); + Files.setAttribute(mountPoint, "dos:hidden", false); + } catch (IOException e) { + LOG.error("Unable to clean up mountpoint {} for Winfsp mounting."); + } + } + } + + private Path getHideaway(Path mountPoint) { + return mountPoint.resolveSibling(mountPoint.getFileName().toString() + "_tmp"); + } + } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 8739be791..fc1962b9d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -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,16 @@ 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 boolean webDavAndWindows; + private final boolean fuseAndWindows; private final WindowsDriveLetters windowsDriveLetters; private final ResourceBundle resourceBundle; + public CheckBox readOnlyCheckbox; public CheckBox customMountFlagsCheckbox; public TextField mountFlags; @@ -53,20 +48,14 @@ public class MountOptionsController implements FxController { public RadioButton mountPointCustomDir; public ChoiceBox 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.webDavAndWindows = settings.preferredVolumeImpl().get() == VolumeImpl.WEBDAV && SystemUtils.IS_OS_WINDOWS; + this.fuseAndWindows = settings.preferredVolumeImpl().get() == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS; 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 +63,11 @@ public class MountOptionsController implements FxController { // readonly: readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode()); - if (getRestrictToStableFuseOnWindows()) { + //TODO: support this feature on Windows + if (fuseAndWindows) { 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 +85,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,20 +176,16 @@ public class MountOptionsController implements FxController { // Getter & Setter - public BooleanProperty osIsWindowsProperty() { - return osIsWindows; - } - public boolean getOsIsWindows() { - return osIsWindows.get(); + return SystemUtils.IS_OS_WINDOWS; } - public BooleanBinding webDavAndWindowsProperty() { + public boolean getCustomMountPointSupported() { return webDavAndWindows; } - public boolean isWebDavAndWindows() { - return webDavAndWindows.get(); + public boolean getReadOnlySupported() { + return fuseAndWindows; } public StringProperty customMountPathProperty() { @@ -212,8 +196,4 @@ public class MountOptionsController implements FxController { return vault.getVaultSettings().customMountPath().get(); } - public Boolean getRestrictToStableFuseOnWindows() { - return restrictToStableFuseOnWindows.get(); - } - } diff --git a/src/main/resources/fxml/vault_options_mount.fxml b/src/main/resources/fxml/vault_options_mount.fxml index a96784020..a0d1ce0e1 100644 --- a/src/main/resources/fxml/vault_options_mount.fxml +++ b/src/main/resources/fxml/vault_options_mount.fxml @@ -42,8 +42,8 @@ - - + + - + From e15b68fc9be800d250739520849c6544668f4bb8 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 28 Feb 2022 18:54:01 +0100 Subject: [PATCH 3/8] Refactor winfsp mount preps and add unit tests --- .../mountpoint/CustomMountPointChooser.java | 45 ++++-- .../CustomMountPointChooserTest.java | 133 ++++++++++++++++++ 2 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index d075e1292..872815a4e 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -13,7 +13,9 @@ 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.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; @@ -21,6 +23,8 @@ import java.util.Optional; class CustomMountPointChooser implements MountPointChooser { + private static final String HIDEAWAY_PREFIX = ".~$"; + private static final String HIDEAWAY_SUFFIX = ".tmp"; private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class); private final VaultSettings vaultSettings; @@ -68,19 +72,41 @@ class CustomMountPointChooser implements MountPointChooser { } } - private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { + void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { //This the case on Windows when using FUSE //See https://github.com/billziss-gh/winfsp/issues/320 assert SystemUtils.IS_OS_WINDOWS; Path hideaway = getHideaway(mountPoint); - if (Files.exists(hideaway)) { - LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount."); - } else if (!Files.isDirectory(mountPoint)) { - throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); //simulate we need a directory - } else { - //TODO: should we require it to be empty? + + var mpExists = Files.exists(mountPoint); + var hideExists = Files.exists(hideaway); + + //both resources exist (whatever type) + //TODO: possible improvement by just deleting an _empty_ hideaway + if (mpExists && hideExists) { + 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 + + if (!Files.isDirectory(hideaway)) { + throw new InvalidMountPointException(new NotDirectoryException(hideaway.toString())); + } + LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { + Files.setAttribute(hideaway, "dos:hidden", true); + } catch (IOException e) { + throw new InvalidMountPointException(e); + } + } else { + if (!Files.isDirectory(mountPoint)) { + throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); + } + try { + if(Files.list(mountPoint).findFirst().isPresent()) { + throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); + } Files.move(mountPoint, hideaway); Files.setAttribute(hideaway, "dos:hidden", true); } catch (IOException e) { @@ -116,8 +142,9 @@ class CustomMountPointChooser implements MountPointChooser { } } - private Path getHideaway(Path mountPoint) { - return mountPoint.resolveSibling(mountPoint.getFileName().toString() + "_tmp"); + //visible for testing + Path getHideaway(Path mountPoint) { + return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX); } } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java new file mode 100644 index 000000000..f5be1522b --- /dev/null +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -0,0 +1,133 @@ +package org.cryptomator.common.mountpoint; + +import org.cryptomator.common.Environment; +import org.cryptomator.common.settings.VaultSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +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.Mockito; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CustomMountPointChooserTest { + + //--- Mocks --- + VaultSettings vaultSettings; + Environment environment; + + CustomMountPointChooser customMpc; + + + @BeforeEach + public void init() { + this.vaultSettings = Mockito.mock(VaultSettings.class); + this.environment = Mockito.mock(Environment.class); + this.customMpc = new CustomMountPointChooser(vaultSettings, environment); + } + + @Nested + class WinfspPreperations { + + @Test + @DisplayName("Test MP preparation for winfsp, 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)); + Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); + Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test MP preparation for winfsp, 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("Test MP preparation for Winfsp, 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)); + Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); + Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test Winfsp MP preparation, 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)); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test Winfsp MP preparation, 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)); + } + + } + + +} From 4f4c9924930a4c1fab67133e21fb863e34c2ba2d Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 14:40:27 +0100 Subject: [PATCH 4/8] Further improvements: * make PARENT_NO_MOUNTPOINT preps system agnostic * add unit tests for cleanup --- .../mountpoint/CustomMountPointChooser.java | 21 ++++---- .../CustomMountPointChooserTest.java | 53 +++++++++++++++++-- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 872815a4e..82898d59e 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -72,13 +72,10 @@ class CustomMountPointChooser implements MountPointChooser { } } + //This the case on Windows when using FUSE + //See https://github.com/billziss-gh/winfsp/issues/320 void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { - //This the case on Windows when using FUSE - //See https://github.com/billziss-gh/winfsp/issues/320 - assert SystemUtils.IS_OS_WINDOWS; - Path hideaway = getHideaway(mountPoint); - var mpExists = Files.exists(mountPoint); var hideExists = Files.exists(hideaway); @@ -104,11 +101,13 @@ class CustomMountPointChooser implements MountPointChooser { throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); } try { - if(Files.list(mountPoint).findFirst().isPresent()) { + if (Files.list(mountPoint).findFirst().isPresent()) { throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); } Files.move(mountPoint, hideaway); - Files.setAttribute(hideaway, "dos:hidden", true); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(hideaway, "dos:hidden", true); + } } catch (IOException e) { throw new InvalidMountPointException(e); } @@ -131,13 +130,15 @@ class CustomMountPointChooser implements MountPointChooser { @Override public void cleanup(Volume caller, Path mountPoint) { - if (VolumeImpl.FUSE == caller.getImplementationType() && MountPointRequirement.PARENT_NO_MOUNT_POINT == caller.getMountPointRequirement()) { + if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) { Path hideaway = getHideaway(mountPoint); try { Files.move(hideaway, mountPoint); - Files.setAttribute(mountPoint, "dos:hidden", false); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(mountPoint, "dos:hidden", false); + } } catch (IOException e) { - LOG.error("Unable to clean up mountpoint {} for Winfsp mounting."); + LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e); } } } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index f5be1522b..2126b16b6 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -1,7 +1,10 @@ package org.cryptomator.common.mountpoint; +import org.apache.commons.lang3.SystemUtils; 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.Assumptions; import org.junit.jupiter.api.BeforeEach; @@ -10,6 +13,7 @@ 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; @@ -21,12 +25,14 @@ 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); @@ -36,7 +42,7 @@ public class CustomMountPointChooserTest { class WinfspPreperations { @Test - @DisplayName("Test MP preparation for winfsp, if only mountpoint is present") + @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"); @@ -58,7 +64,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test MP preparation for winfsp, if only non-empty mountpoint is present") + @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"); @@ -73,7 +79,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test MP preparation for Winfsp, if for any reason only hideaway dir is present") + @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"); @@ -93,7 +99,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test Winfsp MP preparation, if mountpoint and hideaway dirs are present") + @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"); @@ -113,7 +119,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test Winfsp MP preparation, if neither mountpoint nor hideaway dir is present") + @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"); @@ -127,6 +133,43 @@ public class CustomMountPointChooserTest { 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)); + + Assumptions.assumeTrue(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 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)); + } + } + } From f148973bef18bbdf64f80ff958ad74cec80ace67 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 14:50:26 +0100 Subject: [PATCH 5/8] fix wrong visibillity --- .../common/mountpoint/CustomMountPointChooserTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2126b16b6..2ee969363 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -39,7 +39,7 @@ public class CustomMountPointChooserTest { } @Nested - class WinfspPreperations { + public class WinfspPreperations { @Test @DisplayName("PARENT_NO_MOUNTPOINT preparations succeeds, if only mountpoint is present") From fba0df10f9503f0460aa0311d09de6710f13b9f9 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 15:28:01 +0100 Subject: [PATCH 6/8] Resolve code smells and a bug Co-authored-by: sonarcloud --- .../mountpoint/CustomMountPointChooser.java | 51 ++++++++++--------- .../vaultoptions/MountOptionsController.java | 4 +- .../CustomMountPointChooserTest.java | 18 +++++-- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 82898d59e..d174e5dc6 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -12,7 +12,6 @@ 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.NoSuchFileException; @@ -25,6 +24,7 @@ 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; @@ -79,34 +79,27 @@ class CustomMountPointChooser implements MountPointChooser { var mpExists = Files.exists(mountPoint); var hideExists = Files.exists(hideaway); - //both resources exist (whatever type) //TODO: possible improvement by just deleting an _empty_ hideaway - if (mpExists && hideExists) { + 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 - - if (!Files.isDirectory(hideaway)) { - throw new InvalidMountPointException(new NotDirectoryException(hideaway.toString())); - } + isDirectory(hideaway); LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { - Files.setAttribute(hideaway, "dos:hidden", true); + Files.setAttribute(hideaway, WIN_HIDDEN, true); } catch (IOException e) { throw new InvalidMountPointException(e); } - } else { - if (!Files.isDirectory(mountPoint)) { - throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); - } + } else { //only mountpoint exists try { - if (Files.list(mountPoint).findFirst().isPresent()) { - throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); - } + isDirectory(mountPoint); + isEmpty(mountPoint); + Files.move(mountPoint, hideaway); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(hideaway, "dos:hidden", true); + Files.setAttribute(hideaway, WIN_HIDDEN, true); } } catch (IOException e) { throw new InvalidMountPointException(e); @@ -116,13 +109,9 @@ class CustomMountPointChooser implements MountPointChooser { 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 ds = Files.newDirectoryStream(mountPoint)) { - if (ds.iterator().hasNext()) { - throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); - } + isDirectory(mountPoint); + try { + isEmpty(mountPoint); } catch (IOException exception) { throw new InvalidMountPointException("IOException while checking folder content", exception); } @@ -135,7 +124,7 @@ class CustomMountPointChooser implements MountPointChooser { try { Files.move(hideaway, mountPoint); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(mountPoint, "dos:hidden", false); + Files.setAttribute(mountPoint, WIN_HIDDEN, false); } } catch (IOException e) { LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e); @@ -143,6 +132,20 @@ class CustomMountPointChooser implements MountPointChooser { } } + private void isDirectory(Path toCheck) throws InvalidMountPointException { + if (!Files.isDirectory(toCheck)) { + throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString())); + } + } + + private void isEmpty(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); diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index bce186511..3be38568d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -183,7 +183,7 @@ public class MountOptionsController implements FxController { } public boolean isReadOnlySupported() { - return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) ; + return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()); } public StringProperty customMountPathProperty() { @@ -191,7 +191,7 @@ public class MountOptionsController implements FxController { } public boolean isCustomMountOptionsSupported() { - return !(usedVolumeImpl == VolumeImpl.WEBDAV); + return usedVolumeImpl != VolumeImpl.WEBDAV; } public String getCustomMountPath() { diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2ee969363..2954c9355 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -1,6 +1,5 @@ package org.cryptomator.common.mountpoint; -import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.MountPointRequirement; @@ -41,6 +40,19 @@ public class CustomMountPointChooserTest { @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 { @@ -56,8 +68,6 @@ public class CustomMountPointChooserTest { Path hideaway = customMpc.getHideaway(mntPoint); Assertions.assertTrue(Files.exists(hideaway)); - Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); - Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); @@ -91,8 +101,6 @@ public class CustomMountPointChooserTest { //evaluate Assertions.assertTrue(Files.exists(hideaway)); - Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); - Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); From 14dc026354730ede3866e2f167166d0e1bd63c09 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 17:24:54 +0100 Subject: [PATCH 7/8] Cleanup Co-authored-by: Sebastian Stenzel --- .../mountpoint/CustomMountPointChooser.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index d174e5dc6..9cfc8652f 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -14,6 +14,7 @@ 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; @@ -49,16 +50,16 @@ class CustomMountPointChooser implements MountPointChooser { @Override public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException { - switch (caller.getMountPointRequirement()) { + return switch (caller.getMountPointRequirement()) { case PARENT_NO_MOUNT_POINT -> { prepareParentNoMountPoint(mountPoint); LOG.debug("Successfully checked custom mount point: {}", mountPoint); - return true; + yield true; } case EMPTY_MOUNT_POINT -> { prepareEmptyMountPoint(mountPoint); LOG.debug("Successfully checked custom mount point: {}", mountPoint); - return false; + yield false; } case NONE -> { //Requirement "NONE" doesn't make any sense here. @@ -69,15 +70,15 @@ class CustomMountPointChooser implements MountPointChooser { //Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT" throw new InvalidMountPointException(new IllegalStateException("Not implemented")); } - } + }; } - //This the case on Windows when using FUSE + //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); - var hideExists = Files.exists(hideaway); + 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) @@ -85,21 +86,23 @@ class CustomMountPointChooser implements MountPointChooser { } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString())); } else if (!mpExists) { //only hideaway exists - isDirectory(hideaway); + checkIsDirectory(hideaway); LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { - Files.setAttribute(hideaway, WIN_HIDDEN, true); + 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 { - isDirectory(mountPoint); - isEmpty(mountPoint); + checkIsDirectory(mountPoint); + checkIsEmpty(mountPoint); Files.move(mountPoint, hideaway); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(hideaway, WIN_HIDDEN, true); + Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS); } } catch (IOException e) { throw new InvalidMountPointException(e); @@ -109,9 +112,9 @@ class CustomMountPointChooser implements MountPointChooser { private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException { //This is the case for Windows when using Dokany and for Linux and Mac - isDirectory(mountPoint); + checkIsDirectory(mountPoint); try { - isEmpty(mountPoint); + checkIsEmpty(mountPoint); } catch (IOException exception) { throw new InvalidMountPointException("IOException while checking folder content", exception); } @@ -132,13 +135,13 @@ class CustomMountPointChooser implements MountPointChooser { } } - private void isDirectory(Path toCheck) throws InvalidMountPointException { - if (!Files.isDirectory(toCheck)) { + private void checkIsDirectory(Path toCheck) throws InvalidMountPointException { + if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) { throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString())); } } - private void isEmpty(Path toCheck) throws InvalidMountPointException, IOException { + 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())); From 9b001b507155fddea7494409064e3998f8f9d77e Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 3 Mar 2022 17:45:21 +0100 Subject: [PATCH 8/8] Co-authored-by: Sebastian Stenzel --- .../mountpoint/CustomMountPointChooser.java | 8 +------ .../CustomMountPointChooserTest.java | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 9cfc8652f..80c5b067b 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -61,15 +61,9 @@ class CustomMountPointChooser implements MountPointChooser { LOG.debug("Successfully checked custom mount point: {}", mountPoint); yield false; } - case NONE -> { - //Requirement "NONE" doesn't make any sense here. - //No need to prepare/verify a Mountpoint without requiring one... + case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> { throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement")); } - default -> { - //Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT" - throw new InvalidMountPointException(new IllegalStateException("Not implemented")); - } }; } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2954c9355..da2e0fde0 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -5,7 +5,6 @@ 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.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -69,8 +68,9 @@ public class CustomMountPointChooserTest { Path hideaway = customMpc.getHideaway(mntPoint); Assertions.assertTrue(Files.exists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -102,8 +102,9 @@ public class CustomMountPointChooserTest { //evaluate Assertions.assertTrue(Files.exists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -122,8 +123,9 @@ public class CustomMountPointChooserTest { Assertions.assertTrue(Files.exists(hideaway)); Assertions.assertTrue(Files.exists(mntPoint)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -158,8 +160,9 @@ public class CustomMountPointChooserTest { Assertions.assertTrue(Files.exists(mntPoint)); Assertions.assertTrue(Files.notExists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden")); + } } @Test