mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-18 08:36:52 -04:00
adapt to new APIs
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator-Dev/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator-Dev/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.pluginDir="~/AppData/Roaming/Cryptomator-Dev/Plugins" -Dcryptomator.integrationsWin.keychainPaths="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.p12Path="~/AppData/Roaming/Cryptomator-Dev/key.p12" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m --enable-preview" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
6
pom.xml
6
pom.xml
@@ -29,13 +29,13 @@
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptolib.version>2.1.0-rc1</cryptomator.cryptolib.version>
|
||||
<cryptomator.cryptofs.version>2.4.5</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.1.0</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.version>1.2.0-beta1</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>1.3.4</cryptomator.fuse.version>
|
||||
<cryptomator.fuse.version>2.0.0-beta1</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.2.8</cryptomator.webdav.version>
|
||||
<cryptomator.webdav.version>2.0.0-beta1</cryptomator.webdav.version>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
|
||||
@@ -10,19 +10,17 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.keychain.KeychainModule;
|
||||
import org.cryptomator.common.mount.MountModule;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListModule;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -34,7 +32,7 @@ import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
|
||||
public abstract class CommonsModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
||||
@@ -138,13 +136,4 @@ public abstract class CommonsModule {
|
||||
});
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static WebDavServer provideWebDavServer(ObservableValue<InetSocketAddress> serverSocketAddressBinding) {
|
||||
WebDavServer server = WebDavServer.create();
|
||||
// no need to unsubscribe eventually, because server is a singleton
|
||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
30
src/main/java/org/cryptomator/common/mount/MountModule.java
Normal file
30
src/main/java/org/cryptomator/common/mount/MountModule.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.common.mount;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.mount.MountService;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.util.List;
|
||||
|
||||
@Module
|
||||
public class MountModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static List<MountService> provideSupportedMountServices() {
|
||||
return MountService.get().toList();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ObservableValue<MountService> provideMountService(Settings settings, List<MountService> serviceImpls) {
|
||||
return settings.mountService().map(desiredServiceImpl -> {
|
||||
var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
|
||||
return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
package org.cryptomator.common.mount;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class AvailableDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
@Inject
|
||||
public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return this.windowsDriveLetters.getDesiredAvailableDriveLetterPath();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
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;
|
||||
|
||||
class CustomDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
@Inject
|
||||
public CustomDriveLetterChooser(VaultSettings vaultSettings) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
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;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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.Paths;
|
||||
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;
|
||||
|
||||
@Inject
|
||||
public CustomMountPointChooser(VaultSettings vaultSettings) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return caller.getImplementationType() != VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
//VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
|
||||
return this.vaultSettings.getCustomMountPath().map(Paths::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
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"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//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
|
||||
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 {
|
||||
waitForMountpointRestoration(mountPoint);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
|
||||
private void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
|
||||
int attempts = 0;
|
||||
while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
||||
attempts++;
|
||||
if (attempts >= 10) {
|
||||
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 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
public class InvalidMountPointException extends Exception {
|
||||
|
||||
public InvalidMountPointException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class MacVolumeMountChooser implements MountPointChooser {
|
||||
|
||||
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_MAC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) {
|
||||
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
|
||||
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
|
||||
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
|
||||
// Therefore we don't need to prepare anything.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import dagger.multibindings.IntKey;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* Base interface for the Mountpoint-Choosing-Operation that results in the choice and
|
||||
* preparation of a mountpoint or an exception otherwise.<br>
|
||||
* <p>All <i>MountPointChoosers (MPCs)</i> need to implement this class and must be added to
|
||||
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
|
||||
* The MountPointChooserModule will sort them according to their {@link IntKey IntKey priority.}
|
||||
* The priority must be defined by the developer to reflect a useful execution order.<br>
|
||||
* A specific priority <b>must not</b> be assigned to more than one MPC at a time;
|
||||
* the result of having two MPCs with equal priority is undefined.
|
||||
*
|
||||
* <p>MPCs are executed by a {@link Volume} in descending order of their priority
|
||||
* (higher priorities are tried first) to find and prepare a suitable mountpoint for the volume.
|
||||
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
|
||||
* that is provided by the Module. The Set contains all available Choosers, even if they
|
||||
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
|
||||
* check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
|
||||
* <i>before</i> calling {@code #chooseMountPoint(Volume)}.
|
||||
*
|
||||
* <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
|
||||
* according to the MPC's <i>strategy.</i> The <i>strategy</i> can involve reading configs,
|
||||
* searching the filesystem, processing user-input or similar operations.
|
||||
* If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
|
||||
* {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
|
||||
* MountPoint is verified and/or prepared. In this case <i>no other MPC's will be called for
|
||||
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
|
||||
*
|
||||
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
|
||||
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
|
||||
* This is repeated until<br>
|
||||
* <ul>
|
||||
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
|
||||
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
|
||||
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
|
||||
* </ul>
|
||||
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
|
||||
* Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
|
||||
* before throwing the exception.
|
||||
* If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
|
||||
* remaining cleanup.
|
||||
*/
|
||||
public interface MountPointChooser {
|
||||
|
||||
/**
|
||||
* Called by the {@link Volume} to determine whether this MountPointChooser is
|
||||
* applicable for mounting the Vault/Volume, especially with regard to the
|
||||
* current system configuration and particularities of the Volume type.
|
||||
*
|
||||
* <p>Developers should override this method to check for system configurations
|
||||
* that are unsuitable for this MPC.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to determine applicability of the MPC
|
||||
* @return a boolean flag; true if applicable, else false.
|
||||
* @see #chooseMountPoint(Volume)
|
||||
*/
|
||||
boolean isApplicable(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to choose a mountpoint according to the
|
||||
* MountPointChoosers strategy.
|
||||
*
|
||||
* <p>This method must only be called for MPCs that were deemed
|
||||
* {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
|
||||
* Developers should override this method to find or extract a mountpoint for
|
||||
* the volume <b>without</b> preparing it. Preparation should be done by
|
||||
* {@link #prepare(Volume, Path)} instead.
|
||||
* Exceptions in this method should be handled gracefully and result in returning
|
||||
* {@link Optional#empty()} instead of throwing an exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to choose a mountpoint
|
||||
* @return the chosen path or {@link Optional#empty()} if an exception occurred
|
||||
* or no mountpoint could be found.
|
||||
* @see #isApplicable(Volume)
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
Optional<Path> chooseMountPoint(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
|
||||
* This method is only called if the {@link #chooseMountPoint(Volume)} method
|
||||
* of the same MountPointChooser returned a path.
|
||||
*
|
||||
* <p>Developers should override this method to prepare the mountpoint for
|
||||
* the volume and check for any obstacles that could hinder the mount operation.
|
||||
* The mountpoint is deemed "prepared" if it can be used to mount a volume
|
||||
* without any further filesystem actions or user interaction. If this is not possible,
|
||||
* this method should fail. In other words: This method should not return without
|
||||
* either failing or finalizing the preparation of the mountpoint.
|
||||
* Generally speaking exceptions should be wrapped as
|
||||
* {@link InvalidMountPointException} to allow efficient handling by the caller.
|
||||
*
|
||||
* <p>Often the preparation of a mountpoint involves creating files or others
|
||||
* actions that require cleaning up after the volume is unmounted.
|
||||
* In this case developers should override the {@link #cleanup(Volume, Path)}
|
||||
* method and return {@code true} to the volume to indicate that the
|
||||
* {@code #cleanup} method of this MPC should be called after unmount.
|
||||
*
|
||||
* <p><b>Please note:</b> If this method fails the entire
|
||||
* Mountpoint-Choosing-Operation is aborted without calling
|
||||
* {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
|
||||
* do all necessary cleanup before throwing the exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to prepare a mountpoint
|
||||
* @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
|
||||
* @return a boolean flag; true if cleanup is needed, false otherwise
|
||||
* @throws InvalidMountPointException if the preparation fails
|
||||
* @see #chooseMountPoint(Volume)
|
||||
* @see #cleanup(Volume, Path)
|
||||
*/
|
||||
default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
return false; //NO-OP
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to do any cleanup needed after unmount.
|
||||
*
|
||||
* <p>This method is only called if the {@link #prepare(Volume, Path)} method
|
||||
* of the same MountPointChooser returned {@code true}. Typically developers want to
|
||||
* delete any files created prior to mount or do similar tasks.<br>
|
||||
* Exceptions in this method should be handled gracefully.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to cleanup the prepared mountpoint
|
||||
* @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
default void cleanup(Volume caller, Path mountPoint) {
|
||||
//NO-OP
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntKey;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.PerVault;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
|
||||
* See there for additional information.
|
||||
*
|
||||
* @see MountPointChooser
|
||||
*/
|
||||
@Module
|
||||
public abstract class MountPointChooserModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(1000)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(900)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(800)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(101)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(100)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@Named("orderedMountPointChoosers")
|
||||
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
|
||||
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
|
||||
sortedChoosers.putAll(choosers);
|
||||
return Iterables.unmodifiableIterable(sortedChoosers.values());
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
class MountPointHelper {
|
||||
|
||||
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
|
||||
private final Optional<Path> tmpMountPointDir;
|
||||
private volatile boolean unmountDebrisCleared = false;
|
||||
|
||||
@Inject
|
||||
public MountPointHelper(Environment env) {
|
||||
this.tmpMountPointDir = env.getMountPointsDir();
|
||||
}
|
||||
|
||||
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
|
||||
String basename = vaultSettings.mountName().get();
|
||||
//regular
|
||||
Path mountPoint = parentDir.resolve(basename);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id
|
||||
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id and count
|
||||
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
||||
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
|
||||
return; // nothing to do
|
||||
}
|
||||
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
||||
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
||||
}
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
|
||||
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
||||
IOException cleanupFailed = new IOException("Cleanup failed");
|
||||
|
||||
try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
|
||||
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
|
||||
for (Path p : ds) {
|
||||
try {
|
||||
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
|
||||
Files.delete(p);
|
||||
} else if (attr.isDirectory()) {
|
||||
deleteEmptyDir(p);
|
||||
} else if (attr.isSymbolicLink()) {
|
||||
deleteDeadLink(p);
|
||||
} else {
|
||||
LOG.debug("Found non-directory element in mountpoint dir: {}", p);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
cleanupFailed.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanupFailed.getSuppressed().length > 0) {
|
||||
throw cleanupFailed;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
||||
} finally {
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEmptyDir(Path dir) throws IOException {
|
||||
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
||||
try {
|
||||
ensureIsEmpty(dir);
|
||||
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
||||
} catch (DirectoryNotEmptyException e) {
|
||||
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDeadLink(Path symlink) throws IOException {
|
||||
assert Files.isSymbolicLink(symlink);
|
||||
if (Files.notExists(symlink)) { // following link: target does not exist
|
||||
Files.delete(symlink);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureIsEmpty(Path dir) throws IOException {
|
||||
try (var ds = Files.newDirectoryStream(dir)) {
|
||||
if (ds.iterator().hasNext()){
|
||||
throw new DirectoryNotEmptyException(dir.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class TemporaryMountPointChooser implements MountPointChooser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
if (this.environment.getMountPointsDir().isEmpty()) {
|
||||
LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
assert environment.getMountPointsDir().isPresent();
|
||||
//clean leftovers of not-regularly unmounted vaults
|
||||
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
||||
helper.clearIrregularUnmountDebrisIfNeeded();
|
||||
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
try {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint.getParent());
|
||||
LOG.debug("Successfully created folder for mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
case EMPTY_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint);
|
||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
||||
return true;
|
||||
}
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
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"));
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Volume caller, Path mountPoint) {
|
||||
try {
|
||||
Files.delete(mountPoint);
|
||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,6 +74,9 @@ public class Settings {
|
||||
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
|
||||
|
||||
|
||||
private final StringProperty mountService = new SimpleStringProperty();
|
||||
|
||||
|
||||
private Consumer<Settings> saveCmd;
|
||||
|
||||
/**
|
||||
@@ -105,6 +108,7 @@ public class Settings {
|
||||
windowHeight.addListener(this::somethingChanged);
|
||||
displayConfiguration.addListener(this::somethingChanged);
|
||||
language.addListener(this::somethingChanged);
|
||||
mountService.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
@@ -165,6 +169,10 @@ public class Settings {
|
||||
return preferredVolumeImpl;
|
||||
}
|
||||
|
||||
public StringProperty mountService() {
|
||||
return mountService;
|
||||
}
|
||||
|
||||
public ObjectProperty<UiTheme> theme() {
|
||||
return theme;
|
||||
}
|
||||
@@ -210,4 +218,5 @@ public class Settings {
|
||||
public StringProperty languageProperty() {
|
||||
return language;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
|
||||
out.name("debugMode").value(value.debugMode().get());
|
||||
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
||||
out.name("theme").value(value.theme().get().name());
|
||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||
out.name("keychainProvider").value(value.keychainProvider().get());
|
||||
@@ -60,7 +59,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
out.name("windowHeight").value((value.windowHeightProperty().get()));
|
||||
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
|
||||
out.name("language").value((value.languageProperty().get()));
|
||||
|
||||
out.name("mountService").value(value.mountService().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
@@ -89,7 +88,6 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
|
||||
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
||||
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
|
||||
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
||||
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
|
||||
@@ -103,6 +101,12 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
|
||||
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
|
||||
case "language" -> settings.languageProperty().set(in.nextString());
|
||||
case "mountService" -> {
|
||||
var token = in.peek();
|
||||
if (JsonToken.STRING == token) {
|
||||
settings.mountService().set(in.nextString());
|
||||
}
|
||||
}
|
||||
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: {}", name);
|
||||
@@ -115,15 +119,6 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
|
||||
try {
|
||||
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
|
||||
}
|
||||
}
|
||||
|
||||
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
|
||||
try {
|
||||
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.google.common.io.BaseEncoding;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -21,7 +20,6 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -59,10 +57,19 @@ public class VaultSettings {
|
||||
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
|
||||
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
|
||||
private final StringExpression mountName;
|
||||
private final ObjectProperty<Path> mountPoint = new SimpleObjectProperty<>();
|
||||
|
||||
public VaultSettings(String id) {
|
||||
this.id = Objects.requireNonNull(id);
|
||||
this.mountName = StringExpression.stringExpression(displayName.map(VaultSettings::normalizeDisplayName).orElse(""));
|
||||
this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
|
||||
final String name;
|
||||
if (displayName.isEmpty().get()) {
|
||||
name = path.get().getFileName().toString();
|
||||
} else {
|
||||
name = displayName.get();
|
||||
}
|
||||
return normalizeDisplayName(name);
|
||||
}, displayName, path));
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
@@ -190,4 +197,11 @@ public class VaultSettings {
|
||||
}
|
||||
}
|
||||
|
||||
public Path getMountPoint() {
|
||||
return mountPoint.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Path> mountPointProperty() {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AbstractVolume implements Volume {
|
||||
|
||||
private final Iterable<MountPointChooser> choosers;
|
||||
|
||||
protected Path mountPoint;
|
||||
private boolean cleanupRequired;
|
||||
private MountPointChooser usedChooser;
|
||||
|
||||
public AbstractVolume(Iterable<MountPointChooser> choosers) {
|
||||
this.choosers = choosers;
|
||||
}
|
||||
|
||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
||||
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
|
||||
for (var chooser : applicableChoosers) {
|
||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
||||
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
||||
continue;
|
||||
}
|
||||
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
|
||||
this.usedChooser = chooser;
|
||||
return chosenPath.get();
|
||||
}
|
||||
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
|
||||
}
|
||||
|
||||
protected void cleanupMountPoint() {
|
||||
if (this.cleanupRequired) {
|
||||
this.usedChooser.cleanup(this, this.mountPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -39,7 +41,7 @@ public class AutoLocker {
|
||||
try {
|
||||
vault.lock(false);
|
||||
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
} catch (UnmountFailedException | IOException e) {
|
||||
LOG.error("Autolocking failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@interface DefaultMountFlags {
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
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.dokany.DokanyMountFailedException;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
||||
|
||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.DOKANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
try {
|
||||
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
||||
} catch (DokanyMountFailedException e) {
|
||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (IllegalStateException e) {
|
||||
throw new VolumeException("Unmount Failed.", e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmountForced() {
|
||||
mount.unmountForced();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return MountFactory.isApplicable();
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
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;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||
import org.cryptomator.frontend.fuse.mount.Mounter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
|
||||
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(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
mount(fs.getPath("/"), mountFlags, onExitAction);
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
.withFlags(splitFlags(mountFlags)) //
|
||||
.withMountPoint(mountPoint) //
|
||||
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
||||
.build();
|
||||
this.mount = mounter.mount(root, envVars, onExitAction);
|
||||
} catch (FuseMountException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] splitFlags(String str) {
|
||||
List<String> flags = new ArrayList<>();
|
||||
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
|
||||
while (matches.hasNext()) {
|
||||
String flag = matches.next().group();
|
||||
// check if flag is missing its argument:
|
||||
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
|
||||
// next is "double quoted" and flag is missing its argument
|
||||
flag += matches.next().group(1);
|
||||
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
|
||||
// next is 'single quoted' and flag is missing its argument
|
||||
flag += matches.next().group(2);
|
||||
}
|
||||
flags.add(flag);
|
||||
}
|
||||
return flags.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.unmountForced();
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return FuseVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.FUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
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() {
|
||||
return FuseMountFactory.isFuseSupported();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public class LockNotCompletedException extends Exception {
|
||||
|
||||
public LockNotCompletedException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public LockNotCompletedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
/**
|
||||
* Enumeration used to indicate the requirements for mounting a vault
|
||||
* using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint must not exist.
|
||||
*/
|
||||
PARENT_NO_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint may exist.
|
||||
*/
|
||||
PARENT_OPT_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* The actual Mountpoint must exist and must be empty.
|
||||
*/
|
||||
EMPTY_MOUNT_POINT;
|
||||
}
|
||||
@@ -11,9 +11,8 @@ package org.cryptomator.common.vaults;
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
@@ -22,28 +21,34 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.integrations.mount.Mount;
|
||||
import org.cryptomator.integrations.mount.MountBuilder;
|
||||
import org.cryptomator.integrations.mount.MountCapability;
|
||||
import org.cryptomator.integrations.mount.MountFailedException;
|
||||
import org.cryptomator.integrations.mount.MountService;
|
||||
import org.cryptomator.integrations.mount.Mountpoint;
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@PerVault
|
||||
@@ -53,12 +58,13 @@ public class Vault {
|
||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
||||
|
||||
private final Settings settings;
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Provider<Volume> volumeProvider;
|
||||
private final StringBinding defaultMountFlags;
|
||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||
private final VaultState state;
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final ObservableValue<MountService> mountService;
|
||||
private final ObservableValue<String> defaultMountFlags;
|
||||
private final VaultConfigCache configCache;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayablePath;
|
||||
@@ -69,20 +75,20 @@ public class Vault {
|
||||
private final BooleanBinding needsMigration;
|
||||
private final BooleanBinding unknownError;
|
||||
private final StringBinding accessPoint;
|
||||
private final BooleanBinding accessPointPresent;
|
||||
private final BooleanProperty showingStats;
|
||||
|
||||
private volatile Volume volume;
|
||||
private AtomicReference<MountHandle> mount = new AtomicReference<>(null);
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
Vault(Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, ObservableValue<MountService> mountService, VaultStats stats) {
|
||||
this.settings = settings;
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.configCache = configCache;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
this.cryptoFileSystem = cryptoFileSystem;
|
||||
this.state = state;
|
||||
this.lastKnownException = lastKnownException;
|
||||
this.mountService = mountService;
|
||||
this.defaultMountFlags = Bindings.createStringBinding(() -> mountService.getValue().getDefaultMountFlags(vaultSettings.mountName().get()), vaultSettings.mountName(), mountService).orElse(""); //TODO: logic correct?
|
||||
this.stats = stats;
|
||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||
@@ -92,7 +98,6 @@ public class Vault {
|
||||
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
|
||||
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
|
||||
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
|
||||
this.accessPointPresent = this.accessPoint.isNotEmpty();
|
||||
this.showingStats = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
@@ -142,7 +147,28 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
private MountBuilder prepareMount(Path cryptoRoot) {
|
||||
var mountServiceImpl = mountService.getValue();
|
||||
var builder = mountServiceImpl.forFileSystem(cryptoRoot);
|
||||
|
||||
for (var capabiltiy : mountServiceImpl.capabilities()) {
|
||||
switch (capabiltiy) {
|
||||
case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings?
|
||||
case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property
|
||||
case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
|
||||
case MOUNT_FLAGS -> builder.setMountFlags(mountServiceImpl.getDefaultMountFlags(vaultSettings.mountName().get())); //TODO: currently not adjustable
|
||||
case VOLUME_ID -> builder.setVolumeId(vaultSettings.mountName().get());
|
||||
}
|
||||
}
|
||||
|
||||
if (mountServiceImpl.hasCapability(MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH) && vaultSettings.getMountPoint() == null) {
|
||||
return builder;
|
||||
} else {
|
||||
return builder.setMountpoint(vaultSettings.getMountPoint());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
|
||||
if (cryptoFileSystem.get() != null) {
|
||||
throw new IllegalStateException("Already unlocked.");
|
||||
}
|
||||
@@ -150,9 +176,10 @@ public class Vault {
|
||||
boolean success = false;
|
||||
try {
|
||||
cryptoFileSystem.set(fs);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
|
||||
success = true;
|
||||
var rootPath = fs.getRootDirectories().iterator().next();
|
||||
var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED);
|
||||
var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount);
|
||||
success = mount.compareAndSet(null, mountHandle);
|
||||
} finally {
|
||||
if (!success) {
|
||||
destroyCryptoFileSystem();
|
||||
@@ -160,37 +187,27 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
private void lockOnVolumeExit(Throwable t) {
|
||||
LOG.info("Unmounted vault '{}'", getDisplayName());
|
||||
destroyCryptoFileSystem();
|
||||
state.set(VaultState.Value.LOCKED);
|
||||
if (t != null) {
|
||||
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
||||
//initiate unmount
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
|
||||
var mountHandle = mount.get();
|
||||
if (mountHandle == null) {
|
||||
//TODO: noop or InvalidStateException?
|
||||
return;
|
||||
}
|
||||
|
||||
if (forced && mountHandle.supportsUnmountForced) {
|
||||
mountHandle.mount.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
mountHandle.mount.unmount();
|
||||
}
|
||||
|
||||
//wait for lockOnVolumeExit to be executed
|
||||
try {
|
||||
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
||||
if (!locked) {
|
||||
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LockNotCompletedException(e);
|
||||
mountHandle.mount.close();
|
||||
} finally {
|
||||
destroyCryptoFileSystem();
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||
volume.reveal(vaultRevealer);
|
||||
LOG.info("Locked vault '{}'", getDisplayName());
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
@@ -278,22 +295,14 @@ public class Vault {
|
||||
}
|
||||
|
||||
public String getAccessPoint() {
|
||||
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
||||
assert volume != null;
|
||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
||||
var mountPoint = mount.get().mount.getMountpoint();
|
||||
if( mountPoint instanceof Mountpoint.WithPath m) {
|
||||
return m.path().toString();
|
||||
} else {
|
||||
return "";
|
||||
return mountPoint.uri().toString();
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanBinding accessPointPresentProperty() {
|
||||
return accessPointPresent;
|
||||
}
|
||||
|
||||
public boolean isAccessPointPresent() {
|
||||
return accessPointPresent.get();
|
||||
}
|
||||
|
||||
public StringBinding displayablePathProperty() {
|
||||
return displayablePath;
|
||||
}
|
||||
@@ -314,7 +323,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isShowingStats() {
|
||||
return accessPointPresent.get();
|
||||
return mount.get() != null;
|
||||
}
|
||||
|
||||
|
||||
@@ -343,18 +352,18 @@ public class Vault {
|
||||
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
||||
}
|
||||
|
||||
public StringBinding defaultMountFlagsProperty() {
|
||||
public ObservableValue<String> defaultMountFlagsProperty() {
|
||||
return defaultMountFlags;
|
||||
}
|
||||
|
||||
public String getDefaultMountFlags() {
|
||||
return defaultMountFlags.get();
|
||||
return defaultMountFlags.getValue();
|
||||
}
|
||||
|
||||
public String getEffectiveMountFlags() {
|
||||
String mountFlags = vaultSettings.mountFlags().get();
|
||||
if (Strings.isNullOrEmpty(mountFlags)) {
|
||||
return getDefaultMountFlags();
|
||||
return ""; //TODO: should the provider provide dem defaults??
|
||||
} else {
|
||||
return mountFlags;
|
||||
}
|
||||
@@ -372,10 +381,6 @@ public class Vault {
|
||||
return vaultSettings.getId();
|
||||
}
|
||||
|
||||
public Optional<Volume> getVolume() {
|
||||
return Optional.ofNullable(this.volume);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Hashcode / Equals
|
||||
// *******************************************************************************/
|
||||
@@ -394,7 +399,15 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* TODO: reactivate/ needed at all?
|
||||
public boolean supportsForcedUnmount() {
|
||||
return volume.supportsForcedUnmount();
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
private record MountHandle(Mount mount, boolean supportsUnmountForced) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,12 @@ package org.cryptomator.common.vaults;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
@PerVault
|
||||
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
|
||||
@Subcomponent(modules = {VaultModule.class})
|
||||
public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@@ -7,27 +7,14 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module
|
||||
@@ -48,130 +35,4 @@ public class VaultModule {
|
||||
return new SimpleObjectProperty<>(initialErrorCause);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
|
||||
return dokanyVolume;
|
||||
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
|
||||
return fuseVolume;
|
||||
} else {
|
||||
if (VolumeImpl.WEBDAV != preferredImpl) {
|
||||
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
|
||||
}
|
||||
assert webDavVolume.isSupported();
|
||||
return webDavVolume;
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@DefaultMountFlags
|
||||
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
||||
StringExpression mountName = vaultSettings.mountName();
|
||||
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
|
||||
|
||||
return Bindings.createStringBinding(() -> {
|
||||
VolumeImpl v = preferredVolumeImpl.get();
|
||||
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
|
||||
return getMacFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
|
||||
return getLinuxFuseDefaultMountFlags(readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getWindowsFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getDokanyDefaultMountFlags(readOnly);
|
||||
} else {
|
||||
return "--flags-supported-on-FUSE-or-DOKANY-only";
|
||||
}
|
||||
}, mountName, readOnly, preferredVolumeImpl);
|
||||
}
|
||||
|
||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||
private String getMacFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_MAC_OSX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
flags.append(" -ordonly");
|
||||
}
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
flags.append(" -oatomic_o_trunc");
|
||||
flags.append(" -oauto_xattr");
|
||||
flags.append(" -oauto_cache");
|
||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
|
||||
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_LINUX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
flags.append(" -oro");
|
||||
}
|
||||
flags.append(" -oauto_unmount");
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
|
||||
// see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were chosen
|
||||
private String getWindowsFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
|
||||
//WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
|
||||
//So for the time being not setting them is the way to go...
|
||||
//See: https://github.com/billziss-gh/winfsp/issues/319
|
||||
if (!readOnly.get()) {
|
||||
flags.append(" -ouid=-1");
|
||||
flags.append(" -ogid=11");
|
||||
}
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
|
||||
//but the option exists. Let's keep this here in case we need it.
|
||||
// flags.append(" -oThreadCount=").append(5);
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
||||
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
flags.append(" --options CURRENT_SESSION");
|
||||
if (readOnly.get()) {
|
||||
flags.append(",WRITE_PROTECTION");
|
||||
}
|
||||
flags.append(" --thread-count 5");
|
||||
flags.append(" --timeout 10000");
|
||||
flags.append(" --allocation-unit-size 4096");
|
||||
flags.append(" --sector-size 4096");
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Takes a Volume and uses it to mount an unlocked vault
|
||||
*/
|
||||
public interface Volume {
|
||||
|
||||
/**
|
||||
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
|
||||
*
|
||||
* @return true if this volume can be mounted
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* Gets the corresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
|
||||
*
|
||||
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
|
||||
*/
|
||||
VolumeImpl getImplementationType();
|
||||
|
||||
/**
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
||||
|
||||
/**
|
||||
* Reveals the mounted volume.
|
||||
* <p>
|
||||
* The given {@code revealer} might be used to do it, but not necessarily.
|
||||
*
|
||||
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
|
||||
* @throws VolumeException
|
||||
*/
|
||||
void reveal(Revealer revealer) throws VolumeException;
|
||||
|
||||
void unmount() throws VolumeException;
|
||||
|
||||
Optional<Path> getMountPoint();
|
||||
|
||||
MountPointRequirement getMountPointRequirement();
|
||||
|
||||
// optional forced unmounting:
|
||||
|
||||
default boolean supportsForcedUnmount() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void unmountForced() throws VolumeException {
|
||||
throw new VolumeException("Operation not supported.");
|
||||
}
|
||||
|
||||
static VolumeImpl[] getCurrentSupportedAdapters() {
|
||||
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
|
||||
case WEBDAV -> WebDavVolume.isSupportedStatic();
|
||||
case DOKANY -> DokanyVolume.isSupportedStatic();
|
||||
case FUSE -> FuseVolume.isSupportedStatic();
|
||||
}).toArray(VolumeImpl[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
|
||||
*/
|
||||
class VolumeException extends Exception {
|
||||
|
||||
public VolumeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public VolumeException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VolumeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides and unifies the different Revealer implementations in the different nio-adapters.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface Revealer {
|
||||
|
||||
void reveal(Path p) throws VolumeException;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.cryptomator.frontend.webdav.mount.MountParams;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter;
|
||||
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
|
||||
private static final String LOCALHOST_ALIAS = "cryptomator-vault";
|
||||
|
||||
private final Provider<WebDavServer> serverProvider;
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Settings settings;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
private Consumer<Throwable> onExitAction;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||
this.serverProvider = serverProvider;
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.settings = settings;
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
startServlet(fs);
|
||||
mountServlet();
|
||||
this.onExitAction = onExitAction;
|
||||
}
|
||||
|
||||
private void startServlet(CryptoFileSystem fs) {
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
if (!server.isRunning()) {
|
||||
server.start();
|
||||
}
|
||||
CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
|
||||
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
|
||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
|
||||
servlet.start();
|
||||
}
|
||||
|
||||
private void mountServlet() throws VolumeException {
|
||||
if (servlet == null) {
|
||||
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
||||
}
|
||||
|
||||
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specific one or there is no free.
|
||||
Supplier<String> driveLetterSupplier;
|
||||
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
driveLetterSupplier = () -> windowsDriveLetters.getDesiredAvailableDriveLetter().orElse(null);
|
||||
} else {
|
||||
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
|
||||
}
|
||||
|
||||
MountParams mountParams = MountParams.create() //
|
||||
.withWindowsDriveLetter(driveLetterSupplier.get()) //
|
||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||
.build();
|
||||
try {
|
||||
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.forced().orElseThrow(IllegalStateException::new).unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return mount.getMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.NONE;
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
try {
|
||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||
if (alias.getHostAddress().equals("127.0.0.1")) {
|
||||
return LOCALHOST_ALIAS;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (servlet != null) {
|
||||
servlet.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
}
|
||||
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -9,7 +8,7 @@ import javafx.application.Application;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class HostServiceRevealer implements Volume.Revealer {
|
||||
public class HostServiceRevealer {
|
||||
|
||||
private final Lazy<Application> application;
|
||||
|
||||
@@ -18,8 +17,7 @@ public class HostServiceRevealer implements Volume.Revealer {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Path p) throws Volume.VolumeException {
|
||||
public void reveal(Path p) {
|
||||
application.get().getHostServices().showDocument(p.toUri().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -11,6 +10,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
@@ -43,7 +43,7 @@ public class VaultService {
|
||||
* @param vault The vault to reveal
|
||||
*/
|
||||
public Task<Vault> createRevealTask(Vault vault) {
|
||||
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
|
||||
Task<Vault> task = new RevealVaultTask(vault);
|
||||
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
||||
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
||||
return task;
|
||||
@@ -106,22 +106,20 @@ public class VaultService {
|
||||
private static class RevealVaultTask extends Task<Vault> {
|
||||
|
||||
private final Vault vault;
|
||||
private final Volume.Revealer revealer;
|
||||
|
||||
/**
|
||||
* @param vault The vault to lock
|
||||
* @param revealer The object to use to show the vault content to the user.
|
||||
*/
|
||||
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
|
||||
public RevealVaultTask(Vault vault) {
|
||||
this.vault = vault;
|
||||
this.revealer = revealer;
|
||||
|
||||
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException {
|
||||
vault.reveal(revealer);
|
||||
protected Vault call() {
|
||||
//vault.reveal(revealer); //TODO: just call hostApplication service
|
||||
//application.get().getHostServices().showDocument(p.toUri().toString());
|
||||
return vault;
|
||||
}
|
||||
}
|
||||
@@ -180,7 +178,7 @@ public class VaultService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
||||
protected Vault call() throws UnmountFailedException, IOException {
|
||||
vault.lock(forced);
|
||||
return vault;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ package org.cryptomator.ui.fxapp;
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -18,6 +17,7 @@ import javafx.collections.ObservableList;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.awt.desktop.QuitStrategy;
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.EventObject;
|
||||
import java.util.Set;
|
||||
@@ -128,10 +128,8 @@ public class FxApplicationTerminator {
|
||||
if (vault.isUnlocked()) {
|
||||
try {
|
||||
vault.lock(true);
|
||||
} catch (Volume.VolumeException e) {
|
||||
} catch (UnmountFailedException | IOException e) {
|
||||
LOG.error("Failed to unmount vault " + vault.getPath(), e);
|
||||
} catch (LockNotCompletedException e) {
|
||||
LOG.error("Failed to lock vault " + vault.getPath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class LockForcedController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isForceSupported() {
|
||||
return vault.supportsForcedUnmount();
|
||||
return false;//vault.supportsForcedUnmount(); TODO
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.cryptomator.ui.lock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
@@ -17,6 +16,7 @@ import javafx.concurrent.Task;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -53,21 +53,21 @@ public class LockWorkflow extends Task<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
|
||||
protected Void call() throws InterruptedException, ExecutionException, IOException {
|
||||
lock(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void lock(boolean forced) throws InterruptedException, ExecutionException {
|
||||
private void lock(boolean forced) throws InterruptedException, ExecutionException, IOException { //TODO: catch or rethrow IOException?
|
||||
try {
|
||||
vault.lock(forced);
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
} catch (UnmountFailedException e) {
|
||||
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
|
||||
retryOrCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void retryOrCancel() throws ExecutionException, InterruptedException {
|
||||
private void retryOrCancel() throws ExecutionException, InterruptedException, IOException {
|
||||
try {
|
||||
boolean forced = askWhetherToUseTheForce().get();
|
||||
lock(forced);
|
||||
@@ -105,7 +105,7 @@ public class LockWorkflow extends Task<Void> {
|
||||
final var throwable = super.getException();
|
||||
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
if (throwable instanceof Volume.VolumeException) {
|
||||
if (throwable instanceof UnmountFailedException) { //TODO: check if correct exception caught
|
||||
lockWindow.setScene(lockFailedScene.get());
|
||||
lockWindow.show();
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.common.settings.WebDavUrlScheme;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.integrations.mount.MountService;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -15,6 +15,7 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ChoiceBox;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.util.StringConverter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
|
||||
@@ -25,25 +26,25 @@ public class VolumePreferencesController implements FxController {
|
||||
private final Settings settings;
|
||||
private final BooleanBinding showWebDavSettings;
|
||||
private final BooleanBinding showWebDavScheme;
|
||||
public ChoiceBox<VolumeImpl> volumeTypeChoiceBox;
|
||||
private final List<MountService> mountProviders;
|
||||
public ChoiceBox<MountService> volumeTypeChoiceBox;
|
||||
public TextField webDavPortField;
|
||||
public Button changeWebDavPortButton;
|
||||
public ChoiceBox<WebDavUrlScheme> webDavUrlSchemeChoiceBox;
|
||||
|
||||
@Inject
|
||||
VolumePreferencesController(Settings settings) {
|
||||
VolumePreferencesController(Settings settings, List<MountService> mountProviders) {
|
||||
this.settings = settings;
|
||||
this.mountProviders = mountProviders;
|
||||
this.showWebDavSettings = Bindings.equal(settings.preferredVolumeImpl(), VolumeImpl.WEBDAV);
|
||||
this.showWebDavScheme = showWebDavSettings.and(new SimpleBooleanProperty(SystemUtils.IS_OS_LINUX)); //TODO: remove SystemUtils
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
volumeTypeChoiceBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
|
||||
if (!volumeTypeChoiceBox.getItems().contains(settings.preferredVolumeImpl().get())) {
|
||||
settings.preferredVolumeImpl().set(VolumeImpl.WEBDAV);
|
||||
}
|
||||
volumeTypeChoiceBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
|
||||
volumeTypeChoiceBox.setConverter(new VolumeImplConverter());
|
||||
volumeTypeChoiceBox.getItems().addAll(mountProviders);
|
||||
volumeTypeChoiceBox.setConverter(new MountServiceConverter());
|
||||
volumeTypeChoiceBox.getSelectionModel().selectFirst(); //TODO
|
||||
volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvide, newProvider) -> settings.mountService().set(newProvider.getClass().getName()));
|
||||
|
||||
webDavPortField.setText(String.valueOf(settings.port().get()));
|
||||
changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));
|
||||
@@ -101,15 +102,15 @@ public class VolumePreferencesController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private static class VolumeImplConverter extends StringConverter<VolumeImpl> {
|
||||
private static class MountServiceConverter extends StringConverter<MountService> {
|
||||
|
||||
@Override
|
||||
public String toString(VolumeImpl impl) {
|
||||
return impl.getDisplayName();
|
||||
public String toString(MountService provider) {
|
||||
return provider== null? "None" : provider.displayName(); //TODO: adjust message
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl fromString(String string) {
|
||||
public MountService fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ public class AwtTrayMenuController implements TrayMenuController {
|
||||
addChildren(menu, items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeOpenMenu(Runnable runnable) {
|
||||
|
||||
}
|
||||
|
||||
private void addChildren(Menu menu, List<TrayMenuItem> items) {
|
||||
for (var item : items) {
|
||||
// TODO: use Pattern Matching for switch, once available
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -27,30 +25,4 @@ public class UnlockInvalidMountPointController implements FxController {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getMountPoint() {
|
||||
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
|
||||
}
|
||||
|
||||
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,12 +2,8 @@ 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;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
@@ -23,9 +19,6 @@ import javafx.concurrent.Task;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
|
||||
/**
|
||||
* A multi-step task that consists of background activities as well as user interaction.
|
||||
@@ -57,7 +50,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
protected Boolean call() throws InterruptedException, IOException, CryptoException {
|
||||
try {
|
||||
attemptUnlock();
|
||||
return true;
|
||||
@@ -67,48 +60,16 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
private void attemptUnlock() throws IOException, CryptoException {
|
||||
try {
|
||||
keyLoadingStrategy.use(vault::unlock);
|
||||
} catch (Exception e) {
|
||||
Throwables.propagateIfPossible(e, IOException.class);
|
||||
Throwables.propagateIfPossible(e, VolumeException.class);
|
||||
Throwables.propagateIfPossible(e, InvalidMountPointException.class);
|
||||
Throwables.propagateIfPossible(e, CryptoException.class);
|
||||
throw new IllegalStateException("unexpected exception type", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
|
||||
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
|
||||
if (cause instanceof NotDirectoryException) {
|
||||
if (requirement == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
|
||||
LOG.error("Unlock failed. Parent folder is missing: {}", cause.getMessage());
|
||||
} else {
|
||||
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
|
||||
}
|
||||
showInvalidMountPointScene();
|
||||
} else if (cause instanceof FileAlreadyExistsException) {
|
||||
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());
|
||||
showInvalidMountPointScene();
|
||||
} else {
|
||||
handleGenericError(impExc);
|
||||
}
|
||||
}
|
||||
|
||||
private void showInvalidMountPointScene() {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
@@ -144,11 +105,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
protected void failed() {
|
||||
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
|
||||
Throwable throwable = super.getException();
|
||||
if (throwable instanceof InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
} else {
|
||||
handleGenericError(throwable);
|
||||
}
|
||||
handleGenericError(throwable);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package org.cryptomator.ui.vaultoptions;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
||||
import org.cryptomator.common.mount.WindowsDriveLetters;
|
||||
import org.cryptomator.integrations.mount.MountCapability;
|
||||
import org.cryptomator.integrations.mount.MountService;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
@@ -35,10 +33,15 @@ public class MountOptionsController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final VolumeImpl usedVolumeImpl;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
private final ObservableValue<Boolean> mountpointDirSupported;
|
||||
private final ObservableValue<Boolean> mountpointDriveLetterSupported;
|
||||
private final ObservableValue<Boolean> mountpointParentSupported; //TODO: use it in GUI
|
||||
private final ObservableValue<Boolean> readOnlySupported;
|
||||
private final ObservableValue<Boolean> mountFlagsSupported;
|
||||
|
||||
public CheckBox readOnlyCheckbox;
|
||||
public CheckBox customMountFlagsCheckbox;
|
||||
public TextField mountFlags;
|
||||
@@ -49,34 +52,26 @@ public class MountOptionsController implements FxController {
|
||||
public ChoiceBox<String> driveLetterSelection;
|
||||
|
||||
@Inject
|
||||
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
|
||||
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue<MountService> mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.usedVolumeImpl = settings.preferredVolumeImpl().get();
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.mountpointDirSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR));
|
||||
this.mountpointDriveLetterSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
|
||||
this.mountpointParentSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
|
||||
this.mountFlagsSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
|
||||
this.readOnlySupported = mountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
|
||||
// readonly:
|
||||
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
|
||||
//TODO: support this feature on Windows
|
||||
if (usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) {
|
||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
||||
readOnlyCheckbox.setDisable(true);
|
||||
}
|
||||
|
||||
// custom mount flags:
|
||||
mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
|
||||
customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags());
|
||||
if (vault.isHavingCustomMountFlags()) {
|
||||
mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
|
||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
||||
} else {
|
||||
mountFlags.textProperty().bind(vault.defaultMountFlagsProperty());
|
||||
}
|
||||
|
||||
// mount point options:
|
||||
mountPoint.selectedToggleProperty().addListener(this::toggleMountPoint);
|
||||
@@ -105,7 +100,7 @@ public class MountOptionsController implements FxController {
|
||||
if (customMountFlagsCheckbox.isSelected()) {
|
||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
||||
mountFlags.textProperty().unbind();
|
||||
vault.setCustomMountFlags(vault.defaultMountFlagsProperty().get());
|
||||
vault.setCustomMountFlags(vault.defaultMountFlagsProperty().getValue());
|
||||
mountFlags.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
|
||||
} else {
|
||||
mountFlags.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags());
|
||||
@@ -125,7 +120,7 @@ public class MountOptionsController implements FxController {
|
||||
try {
|
||||
var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
|
||||
|
||||
if(Files.exists(initialDir)) {
|
||||
if (Files.exists(initialDir)) {
|
||||
directoryChooser.setInitialDirectory(initialDir.toFile());
|
||||
}
|
||||
} catch (InvalidPathException e) {
|
||||
@@ -178,25 +173,46 @@ public class MountOptionsController implements FxController {
|
||||
|
||||
// Getter & Setter
|
||||
|
||||
public boolean isOsWindows() {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
public ObservableValue<Boolean> mountFlagsSupportedProperty() {
|
||||
return mountFlagsSupported;
|
||||
}
|
||||
|
||||
public boolean isCustomMountPointSupported() {
|
||||
return !(usedVolumeImpl == VolumeImpl.WEBDAV && isOsWindows());
|
||||
public boolean isMountFlagsSupported() {
|
||||
return mountFlagsSupported.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> mountpointDirSupportedProperty() {
|
||||
return mountpointDirSupported;
|
||||
}
|
||||
|
||||
public boolean isMountpointDirSupported() {
|
||||
return mountpointDirSupported.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> mountpointParentSupportedProperty() {
|
||||
return mountpointParentSupported;
|
||||
}
|
||||
|
||||
public boolean isMountpointParentSupported() {
|
||||
return mountpointParentSupported.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> mountpointDriveLetterSupportedProperty() {
|
||||
return mountpointDriveLetterSupported;
|
||||
}
|
||||
|
||||
public boolean isMountpointDriveLetterSupported() {
|
||||
return mountpointDriveLetterSupported.getValue();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> readOnlySupportedProperty() {
|
||||
return mountpointDriveLetterSupported;
|
||||
}
|
||||
|
||||
public boolean isReadOnlySupported() {
|
||||
return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows());
|
||||
return readOnlySupported.getValue();
|
||||
}
|
||||
|
||||
public StringProperty customMountPathProperty() {
|
||||
return vault.getVaultSettings().customMountPath();
|
||||
}
|
||||
|
||||
public boolean isCustomMountOptionsSupported() {
|
||||
return usedVolumeImpl != VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
public String getCustomMountPath() {
|
||||
return vault.getVaultSettings().customMountPath().get();
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
<Insets bottom="6" top="6"/>
|
||||
</padding>
|
||||
</Label>
|
||||
<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"/>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
|
||||
|
||||
@@ -18,14 +18,15 @@
|
||||
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
|
||||
<VBox spacing="4" alignment="CENTER_LEFT">
|
||||
<Label text="%main.vaultDetail.revealBtn"/>
|
||||
<Label styleClass="label-extra-small" text="${controller.vault.accessPoint}" textOverrun="CENTER_ELLIPSIS"
|
||||
visible="${controller.vault.accessPointPresent}" managed="${controller.vault.accessPointPresent}"/>
|
||||
<!-- TODO -->
|
||||
<!--Label styleClass="label-extra-small" text="${controller.vault.accessPoint}" textOverrun="CENTER_ELLIPSIS"
|
||||
visible="${controller.vault.accessPoint.empty}" managed="${controller.vault.accessPoint.empty}"/-->
|
||||
</VBox>
|
||||
</HBox>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<!--tooltip>
|
||||
<Tooltip text="${controller.vault.accessPoint}"/>
|
||||
</tooltip>
|
||||
</tooltip -->
|
||||
</Button>
|
||||
<Button text="%main.vaultDetail.lockBtn" minWidth="120" onAction="#lock">
|
||||
<graphic>
|
||||
|
||||
@@ -22,28 +22,33 @@
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly"/>
|
||||
<Label text="Options depend on the selected volume provider in the general preferences"/>
|
||||
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly" visible="${controller.readOnlySupported}" managed="${controller.readOnlySupported}"/>
|
||||
|
||||
<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>
|
||||
<Insets left="24"/>
|
||||
</VBox.margin>
|
||||
</TextField>
|
||||
<VBox visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">
|
||||
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags"/>
|
||||
<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity">
|
||||
<VBox.margin>
|
||||
<Insets left="24"/>
|
||||
</VBox.margin>
|
||||
</TextField>
|
||||
</VBox>
|
||||
|
||||
<Label text="%vaultOptions.mount.mountPoint">
|
||||
<VBox.margin>
|
||||
<Insets top="9"/>
|
||||
</VBox.margin>
|
||||
</Label>
|
||||
|
||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointAuto" text="%vaultOptions.mount.mountPoint.auto"/>
|
||||
<HBox spacing="6" visible="${controller.osWindows}" managed="${controller.osWindows}">
|
||||
|
||||
<HBox spacing="6" visible="${controller.mountpointDriveLetterSupported}" managed="${controller.mountpointDriveLetterSupported}">
|
||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointWinDriveLetter" text="%vaultOptions.mount.mountPoint.driveLetter"/>
|
||||
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointWinDriveLetter.selected}"/>
|
||||
</HBox>
|
||||
<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" />
|
||||
|
||||
<HBox fx:id="customMountPointRadioBtn" spacing="6" alignment="CENTER_LEFT" visible="${controller.mountpointDirSupported}" managed="${controller.mountpointDirSupported}">
|
||||
<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"/>
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public class VaultModuleTest {
|
||||
System.setProperty("user.home", tmpDir.toString());
|
||||
}
|
||||
|
||||
/* TODO: reactivate!
|
||||
@Test
|
||||
@DisplayName("provideDefaultMountFlags on Mac/FUSE")
|
||||
@EnabledOnOs(OS.MAC)
|
||||
@@ -69,4 +70,6 @@ public class VaultModuleTest {
|
||||
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("--options CURRENT_SESSION,WRITE_PROTECTION"));
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user