diff --git a/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java new file mode 100644 index 000000000..fb481167c --- /dev/null +++ b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java @@ -0,0 +1,8 @@ +package org.cryptomator.common.mount; + +public class MountPointPreparationException extends RuntimeException { + + public MountPointPreparationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java new file mode 100644 index 000000000..ca82b54da --- /dev/null +++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java @@ -0,0 +1,110 @@ +package org.cryptomator.common.mount; + +import org.apache.commons.lang3.SystemUtils; +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; + +public final class MountWithinParentUtil { + + private static final Logger LOG = LoggerFactory.getLogger(Mounter.class); + private static final String HIDEAWAY_PREFIX = ".~$"; + private static final String HIDEAWAY_SUFFIX = ".tmp"; + private static final String WIN_HIDDEN_ATTR = "dos:hidden"; + + private MountWithinParentUtil() {} + + static void prepareParentNoMountPoint(Path mountPoint) throws MountPointPreparationException { + 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 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); + + Files.move(mountPoint, hideaway); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS); + } + } catch (IOException e) { + throw new MountPointPreparationException(e); + } + } + } + + static void cleanup(Path mountPoint) { + Path hideaway = getHideaway(mountPoint); + try { + waitForMountpointRestoration(mountPoint); + 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); + } + } + + //on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds + private static void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException { + int attempts = 0; + while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) { + attempts++; + if (attempts >= 5) { + throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " ."); + } + + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared"); + } + } + } + + private static void checkIsDirectory(Path toCheck) throws MountPointPreparationException { + if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) { + throw new MountPointPreparationException(new NotDirectoryException(toCheck.toString())); + } + } + + private static void checkIsEmpty(Path toCheck) throws MountPointPreparationException, IOException { + try (var dirStream = Files.list(toCheck)) { + if (dirStream.findFirst().isPresent()) { + throw new MountPointPreparationException(new DirectoryNotEmptyException(toCheck.toString())); + } + } + } + + //visible for testing + static Path getHideaway(Path mountPoint) { + return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX); + } + +} diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java index bb6aace42..ee5b0a8e4 100644 --- a/src/main/java/org/cryptomator/common/mount/Mounter.java +++ b/src/main/java/org/cryptomator/common/mount/Mounter.java @@ -50,7 +50,7 @@ public class Mounter { this.vaultSettings = vaultSettings; } - MadePreparations prepare() throws IOException { + Runnable prepare() throws IOException { for (var capability : service.capabilities()) { switch (capability) { case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs"); @@ -67,13 +67,13 @@ public class Mounter { return prepareMountPoint(); } - private MadePreparations prepareMountPoint() throws IOException { + private Runnable prepareMountPoint() throws IOException { + Runnable cleanup = () -> {}; var userChosenMountPoint = vaultSettings.getMountPoint(); var defaultMountPointBase = env.getMountPointsDir().orElseThrow(); var canMountToDriveLetter = service.hasCapability(MOUNT_AS_DRIVE_LETTER); var canMountToParent = service.hasCapability(MOUNT_WITHIN_EXISTING_PARENT); var canMountToDir = service.hasCapability(MOUNT_TO_EXISTING_DIR); - boolean mountWithinCustomParent = false; if (userChosenMountPoint == null) { if (service.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) { @@ -89,9 +89,12 @@ public class Mounter { builder.setMountpoint(mountPoint); } } else { - mountWithinCustomParent = canMountToParent && !canMountToDir; - if (mountWithinCustomParent) { - // TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT + if (canMountToParent && !canMountToDir) { + MountWithinParentUtil.prepareParentNoMountPoint(userChosenMountPoint); + cleanup = () -> { + System.out.println("CLEANUP"); + MountWithinParentUtil.cleanup(userChosenMountPoint); + }; } try { builder.setMountpoint(userChosenMountPoint); @@ -104,17 +107,12 @@ public class Mounter { //mountpoint must exist throw new MountPointNotExistsException(e.getMessage()); } else { + //TODO: if (!canMountToDir && canMountToParent && !Files.notExists(userChosenMountPoint)) { throw new IllegalMountPointException(e.getMessage()); } - /* - //TODO: - if (!canMountToDir && canMountToParent && !Files.notExists(userChosenMountPoint)) { - //parent must exist, mountpoint must not exist - } - */ } } - return new MadePreparations(mountWithinCustomParent); + return cleanup; } } @@ -123,22 +121,11 @@ public class Mounter { var mountService = this.mountServiceObservable.getValue().service(); var builder = mountService.forFileSystem(cryptoFsRoot); var internal = new SettledMounter(mountService, builder, vaultSettings); - var preps = internal.prepare(); - return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), preps.mountWithinCustomParent); + var cleanup = internal.prepare(); + return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup); } - public void cleanup(MountHandle handle) { - if(handle.mountWithinCustomParent) { - //TODO - } - } - - - public record MountHandle(Mount mountObj, boolean supportsUnmountForced, boolean mountWithinCustomParent) { - - } - - private record MadePreparations(boolean mountWithinCustomParent) { + public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) { } diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index b17372f16..b7fb0fd4c 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -173,9 +173,7 @@ public class Vault { try { mountHandle.mountObj().close(); - if(mountHandle.mountWithinCustomParent()) { - mounter.cleanup(mountHandle); - } + mountHandle.specialCleanup().run(); } finally { destroyCryptoFileSystem(); }