mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 10:11:27 +00:00
Merge pull request #1307 from cryptomator/feature/fuse-on-win
Added (experimental) support for using FUSE on Windows (using WinFSP)
This commit is contained in:
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="launcher" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Xss2m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dfuse.experimental="true" -Xss2m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -4,6 +4,7 @@ java ^
|
||||
-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" ^
|
||||
-Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" ^
|
||||
-Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" ^
|
||||
-Dcryptomator.mountPointsDir="~/Cryptomator" ^
|
||||
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
|
||||
-Xss20m ^
|
||||
-Xmx512m ^
|
||||
|
||||
@@ -40,6 +40,7 @@ public class Environment {
|
||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
||||
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
||||
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
|
||||
}
|
||||
|
||||
public boolean useCustomLogbackConfig() {
|
||||
@@ -74,6 +75,10 @@ public class Environment {
|
||||
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
|
||||
}
|
||||
|
||||
public boolean useExperimentalFuse() {
|
||||
return Boolean.getBoolean("fuse.experimental");
|
||||
}
|
||||
|
||||
private int getInt(String propertyName, int defaultValue) {
|
||||
String value = System.getProperty(propertyName);
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
|
||||
public class AvailableDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
public static final int PRIORITY = 200;
|
||||
|
||||
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.getAvailableDriveLetterPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CustomDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
public static final int PRIORITY = 100;
|
||||
|
||||
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 int getPriority() {
|
||||
return PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
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.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CustomMountPointChooser implements MountPointChooser {
|
||||
|
||||
public static final int PRIORITY = 0;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
@Inject
|
||||
public CustomMountPointChooser(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
//Disable if useExperimentalFuse is required (Win + Fuse), but set to false
|
||||
return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
|
||||
}
|
||||
|
||||
@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 {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
|
||||
case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
||||
}
|
||||
default -> {
|
||||
//Currently the case for "PARENT_OPT_MOUNT_POINT"
|
||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
||||
}
|
||||
}
|
||||
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This the case on Windows when using FUSE
|
||||
//See https://github.com/billziss-gh/winfsp/issues/320
|
||||
Path parent = mountPoint.getParent();
|
||||
if (!Files.isDirectory(parent)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
|
||||
}
|
||||
//We must use #notExists() here because notExists =/= !exists (see docs)
|
||||
if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
||||
//File exists OR can't be determined
|
||||
throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This is the case for Windows when using Dokany and for Linux and Mac
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while checking folder content", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
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 #getPriority() 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 ascending order of their priority
|
||||
* (smaller 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 extends Comparable<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
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the {@link MountPointChooserModule} to sort the available MPCs
|
||||
* and determine their execution order.
|
||||
* The priority must be defined by the developer to reflect a useful execution order.
|
||||
* MPCs with lower priorities will be placed at lower indices in the resulting
|
||||
* {@link SortedSet} and will be executed with higher probability.<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.
|
||||
*
|
||||
* @return the priority of this MPC.
|
||||
*/
|
||||
int getPriority();
|
||||
|
||||
/**
|
||||
* Called by the {@link Volume} to determine the execution order of the registered MPCs.
|
||||
* <b>Implementations usually may not override this method.</b> This default implementation
|
||||
* sorts the MPCs in ascending order of their {@link #getPriority() priority.}<br>
|
||||
* <br>
|
||||
* <b>Original description:</b>
|
||||
* <p>{@inheritDoc}
|
||||
*
|
||||
* @implNote This default implementation sorts the MPCs in ascending order
|
||||
* of their {@link #getPriority() priority.}
|
||||
*/
|
||||
@Override
|
||||
default int compareTo(MountPointChooser other) {
|
||||
Preconditions.checkNotNull(other, "Other must not be null!");
|
||||
|
||||
//Sort by priority (ascending order)
|
||||
return Integer.compare(this.getPriority(), other.getPriority());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoSet;
|
||||
import org.cryptomator.common.vaults.PerVault;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
|
||||
* See there for additional information.
|
||||
*
|
||||
* @see MountPointChooser
|
||||
*/
|
||||
@Module
|
||||
public abstract class MountPointChooserModule {
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@Named("orderedMountPointChoosers")
|
||||
public static SortedSet<MountPointChooser> provideOrderedMountPointChoosers(Set<MountPointChooser> choosers) {
|
||||
//Sort by natural order. The natural order is defined by MountPointChooser#compareTo
|
||||
return ImmutableSortedSet.copyOf(choosers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TemporaryMountPointChooser implements MountPointChooser {
|
||||
|
||||
public static final int PRIORITY = 300;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
@Inject
|
||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@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) {
|
||||
return this.environment.getMountPointsDir().map(this::choose);
|
||||
}
|
||||
|
||||
private Path choose(Path parent) {
|
||||
String basename = this.vaultSettings.mountName().get();
|
||||
for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
Path mountPoint = parent.resolve(basename + "_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parent, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
// 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.
|
||||
if (SystemUtils.IS_OS_MAC && mountPoint.getParent().equals(Paths.get("/Volumes"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 "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());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,14 @@ public class VaultSettings {
|
||||
return winDriveLetter;
|
||||
}
|
||||
|
||||
public Optional<String> getWinDriveLetter() {
|
||||
String current = this.winDriveLetter.get();
|
||||
if (!Strings.isNullOrEmpty(current)) {
|
||||
return Optional.of(current);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public BooleanProperty unlockAfterStartup() {
|
||||
return unlockAfterStartup;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public abstract class AbstractVolume implements Volume {
|
||||
|
||||
private final SortedSet<MountPointChooser> choosers;
|
||||
|
||||
protected Path mountPoint;
|
||||
|
||||
//Cleanup
|
||||
private boolean cleanupRequired;
|
||||
private MountPointChooser usedChooser;
|
||||
|
||||
public AbstractVolume(SortedSet<MountPointChooser> choosers) {
|
||||
this.choosers = choosers;
|
||||
}
|
||||
|
||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
||||
SortedSet<MountPointChooser> checkedChoosers = new TreeSet<>(); //Natural order
|
||||
for (MountPointChooser chooser : this.choosers) {
|
||||
if(!chooser.isApplicable(this)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
||||
checkedChoosers.add(chooser); //Consider a chooser checked if it's #chooseMountPoint() method was called
|
||||
if (chosenPath.isEmpty()) {
|
||||
//Chooser was applicable, but couldn't find a feasible mountpoint
|
||||
continue;
|
||||
}
|
||||
this.cleanupRequired = chooser.prepare(this, chosenPath.get()); //Fail entirely if an Exception occurs
|
||||
this.usedChooser = chooser;
|
||||
return chosenPath.get();
|
||||
}
|
||||
//SortedSet#stream() should return a sorted stream (that's what it's docs and the docs of #spliterator() say, even if they are not 100% clear for me.)
|
||||
//We want to keep that order, that's why we use ImmutableSet#toImmutableSet() to collect (even if it doesn't implement SortedSet, it's docs promise use encounter ordering.)
|
||||
String checked = Joiner.on(", ").join(checkedChoosers.stream().map((mpc) -> mpc.getClass().getTypeName()).collect(ImmutableSet.toImmutableSet()));
|
||||
throw new InvalidMountPointException(String.format("No feasible MountPoint found! Checked %s", checked.isBlank() ? "<No applicable MPC>" : checked));
|
||||
}
|
||||
|
||||
protected void cleanupMountPoint() {
|
||||
if (this.cleanupRequired) {
|
||||
this.usedChooser.cleanup(this, this.mountPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
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.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
@@ -10,17 +12,11 @@ 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.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Named;
|
||||
import java.util.SortedSet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class DokanyVolume implements Volume {
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
||||
|
||||
@@ -28,24 +24,23 @@ public class DokanyVolume implements Volume {
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountFactory mountFactory;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
private Mount mount;
|
||||
private Path mountPoint;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") SortedSet<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.mountFactory = new MountFactory(executorService);
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.DOKANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
String mountName = vaultSettings.displayName().get();
|
||||
try {
|
||||
@@ -58,36 +53,6 @@ public class DokanyVolume implements Volume {
|
||||
}
|
||||
}
|
||||
|
||||
private Path determineMountPoint() throws VolumeException, IOException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
|
||||
if (optionalCustomMountPoint.isPresent()) {
|
||||
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
||||
checkProvidedMountPoint(customMountPoint);
|
||||
return customMountPoint;
|
||||
} else if (!Strings.isNullOrEmpty(vaultSettings.winDriveLetter().get())) {
|
||||
return Path.of(vaultSettings.winDriveLetter().get().charAt(0) + ":\\");
|
||||
} else {
|
||||
//auto assign drive letter
|
||||
if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
|
||||
return Path.of(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
|
||||
} else {
|
||||
//TODO: Error Handling
|
||||
throw new VolumeException("No free drive letter available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkProvidedMountPoint(Path mountPoint) throws IOException {
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new NotDirectoryException(mountPoint.toString());
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(mountPoint.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
boolean success = mount.reveal();
|
||||
@@ -99,11 +64,17 @@ public class DokanyVolume implements Volume {
|
||||
@Override
|
||||
public void unmount() {
|
||||
mount.close();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
public boolean isSupported() {
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
|
||||
@@ -2,8 +2,9 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
@@ -15,95 +16,36 @@ 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.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import javax.inject.Named;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
|
||||
public class FuseVolume implements Volume {
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac");
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
private Mount fuseMnt;
|
||||
private Path mountPoint;
|
||||
private boolean createdTemporaryMountPoint;
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public FuseVolume(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
public FuseVolume(@Named("orderedMountPointChoosers") SortedSet<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
|
||||
if (optionalCustomMountPoint.isPresent()) {
|
||||
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
||||
checkProvidedMountPoint(customMountPoint);
|
||||
this.mountPoint = customMountPoint;
|
||||
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
|
||||
} else {
|
||||
this.mountPoint = prepareTemporaryMountPoint();
|
||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
||||
}
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
|
||||
mount(fs.getPath("/"), mountFlags);
|
||||
}
|
||||
|
||||
private void checkProvidedMountPoint(Path mountPoint) throws IOException {
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new NotDirectoryException(mountPoint.toString());
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(mountPoint.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Path prepareTemporaryMountPoint() throws IOException, VolumeException {
|
||||
Path mountPoint = chooseNonExistingTemporaryMountPoint();
|
||||
// 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.
|
||||
if (IS_MAC && mountPoint.getParent().equals(Paths.get("/Volumes"))) {
|
||||
return mountPoint;
|
||||
} else {
|
||||
Files.createDirectories(mountPoint);
|
||||
this.createdTemporaryMountPoint = true;
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private Path chooseNonExistingTemporaryMountPoint() throws VolumeException {
|
||||
Path parent = environment.getMountPointsDir().orElseThrow();
|
||||
String basename = vaultSettings.getId();
|
||||
for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
Path mountPoint = parent.resolve(basename + "_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}/{}_x. Giving up after {} attempts.", parent, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
throw new VolumeException("Did not find feasible mount point.");
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
|
||||
.build();
|
||||
this.fuseMnt = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException e) {
|
||||
this.mount = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +57,7 @@ public class FuseVolume implements Volume {
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.revealInFileManager();
|
||||
mount.revealInFileManager();
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
|
||||
throw new VolumeException(e);
|
||||
@@ -130,34 +72,23 @@ public class FuseVolume implements Volume {
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmountForced();
|
||||
fuseMnt.close();
|
||||
mount.unmountForced();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupTemporaryMountPoint();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmount();
|
||||
fuseMnt.close();
|
||||
mount.unmount();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupTemporaryMountPoint();
|
||||
}
|
||||
|
||||
private void cleanupTemporaryMountPoint() {
|
||||
if (createdTemporaryMountPoint) {
|
||||
try {
|
||||
Files.delete(mountPoint);
|
||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,12 +97,17 @@ public class FuseVolume implements Volume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.FUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
|
||||
return FuseMountFactory.isFuseSupported();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -8,6 +8,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface PerVault {
|
||||
public @interface PerVault {
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
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;
|
||||
@@ -33,11 +35,11 @@ import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
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.atomic.AtomicReference;
|
||||
|
||||
@@ -120,16 +122,13 @@ public class Vault {
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
|
||||
if (vaultSettings.useCustomMountPath().get() && Strings.isNullOrEmpty(vaultSettings.customMountPath().get())) {
|
||||
throw new NotDirectoryException("");
|
||||
}
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags());
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws Volume.VolumeException {
|
||||
public synchronized void lock(boolean forced) throws VolumeException {
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
@@ -145,7 +144,7 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal() throws Volume.VolumeException {
|
||||
public void reveal() throws VolumeException {
|
||||
volume.reveal();
|
||||
}
|
||||
|
||||
@@ -318,6 +317,10 @@ public class Vault {
|
||||
return vaultSettings.getId();
|
||||
}
|
||||
|
||||
public Optional<Volume> getVolume() {
|
||||
return Optional.ofNullable(this.volume);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Hashcode / Equals
|
||||
// *******************************************************************************/
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
@@ -14,7 +15,7 @@ import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
|
||||
@PerVault
|
||||
@Subcomponent(modules = {VaultModule.class})
|
||||
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
|
||||
public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@@ -84,6 +84,8 @@ public class VaultModule {
|
||||
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 {
|
||||
@@ -142,6 +144,28 @@ public class VaultModule {
|
||||
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 choosen
|
||||
private String getWindowsFuseDefaultMountFlags(StringBinding 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=-1");
|
||||
}
|
||||
flags.append(" -ovolname=").append(mountName.get());
|
||||
//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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
|
||||
@@ -20,11 +21,17 @@ public interface Volume {
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* Gets the coresponding 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) throws IOException, VolumeException;
|
||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
|
||||
|
||||
void reveal() throws VolumeException;
|
||||
|
||||
@@ -32,6 +39,8 @@ public interface Volume {
|
||||
|
||||
Optional<Path> getMountPoint();
|
||||
|
||||
MountPointRequirement getMountPointRequirement();
|
||||
|
||||
// optional forced unmounting:
|
||||
|
||||
default boolean supportsForcedUnmount() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
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;
|
||||
@@ -101,6 +102,11 @@ public class WebDavVolume implements Volume {
|
||||
return Optional.ofNullable(mountPoint); //TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.NONE;
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
try {
|
||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||
@@ -126,6 +132,11 @@ public class WebDavVolume implements Volume {
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
@@ -12,6 +13,7 @@ import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -24,7 +26,7 @@ public final class WindowsDriveLetters {
|
||||
|
||||
static {
|
||||
try (IntStream stream = IntStream.rangeClosed('C', 'Z')) {
|
||||
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
|
||||
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +48,18 @@ public final class WindowsDriveLetters {
|
||||
}
|
||||
|
||||
public Set<String> getAvailableDriveLetters() {
|
||||
return Sets.difference(C_TO_Z, getOccupiedDriveLetters());
|
||||
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
public Optional<String> getAvailableDriveLetter() {
|
||||
return getAvailableDriveLetters().stream().findFirst();
|
||||
}
|
||||
|
||||
public Optional<Path> getAvailableDriveLetterPath() {
|
||||
return getAvailableDriveLetter().map(this::toPath);
|
||||
}
|
||||
|
||||
public Path toPath(String driveLetter) {
|
||||
return Path.of(driveLetter + ":\\");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>1.9.12</cryptomator.cryptofs.version>
|
||||
<cryptomator.jni.version>2.2.3</cryptomator.jni.version>
|
||||
<cryptomator.fuse.version>1.2.3</cryptomator.fuse.version>
|
||||
<cryptomator.fuse.version>1.2.5</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.1.15</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.0.12</cryptomator.webdav.version>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import dagger.Lazy;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
@@ -11,6 +12,7 @@ import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
|
||||
@UnlockScoped
|
||||
public class UnlockInvalidMountPointController implements FxController {
|
||||
|
||||
@@ -36,4 +38,12 @@ public class UnlockInvalidMountPointController implements FxController {
|
||||
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
|
||||
}
|
||||
|
||||
public boolean getMustExist() {
|
||||
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
|
||||
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
|
||||
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
|
||||
|
||||
return requirement == MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import javafx.concurrent.Task;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
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;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
@@ -28,7 +29,7 @@ import javax.inject.Named;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
@@ -75,7 +76,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
try {
|
||||
if (attemptUnlock()) {
|
||||
handleSuccess();
|
||||
@@ -84,10 +85,10 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
cancel(false); // set Tasks state to cancelled
|
||||
return false;
|
||||
}
|
||||
} catch (NotDirectoryException | DirectoryNotEmptyException e) {
|
||||
} catch (InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
throw e; // rethrow to trigger correct exception handling in Task
|
||||
} catch (CryptoException | Volume.VolumeException | IOException e) {
|
||||
} catch (Exception e) {
|
||||
handleGenericError(e);
|
||||
throw e; // rethrow to trigger correct exception handling in Task
|
||||
} finally {
|
||||
@@ -96,7 +97,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
|
||||
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
|
||||
while (proceed) {
|
||||
try {
|
||||
@@ -155,14 +156,55 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInvalidMountPoint(FileSystemException e) {
|
||||
LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
|
||||
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
|
||||
MountPointRequirement 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)
|
||||
|
||||
Throwable cause = impExc.getCause();
|
||||
//Cause is either null (cause the IMPE was thrown directly, e.g. because no MPC succeeded)
|
||||
//or the cause was not an Exception (but some other kind of Throwable)
|
||||
//Either way: Handle as generic error
|
||||
if (!(cause instanceof Exception)) {
|
||||
handleGenericError(impExc);
|
||||
return;
|
||||
}
|
||||
|
||||
//From here on handle the cause, not the caught exception
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cause instanceof FileAlreadyExistsException) {
|
||||
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
|
||||
showInvalidMountPointScene();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cause instanceof DirectoryNotEmptyException) {
|
||||
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
|
||||
showInvalidMountPointScene();
|
||||
return;
|
||||
}
|
||||
|
||||
//Everything else (especially IOException) results in a generic error
|
||||
//This must be done after the other exceptions because they extend IOException...
|
||||
handleGenericError(cause);
|
||||
}
|
||||
|
||||
private void showInvalidMountPointScene() {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
});
|
||||
}
|
||||
|
||||
private void handleGenericError(Exception e) {
|
||||
private void handleGenericError(Throwable e) {
|
||||
LOG.error("Unlock failed for technical reasons.", e);
|
||||
Platform.runLater(() -> {
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
|
||||
@@ -18,6 +18,7 @@ import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
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;
|
||||
@@ -51,13 +52,20 @@ public class MountOptionsController implements FxController {
|
||||
public RadioButton mountPointCustomDir;
|
||||
public ChoiceBox<String> driveLetterSelection;
|
||||
|
||||
//FUSE + Windows -> Disable some (experimental) features for the user because they are unstable
|
||||
//Use argument Dfuse.experimental="true" to override
|
||||
private final BooleanBinding restrictToStableFuseOnWindows;
|
||||
|
||||
@Inject
|
||||
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
|
||||
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.webDavAndWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.WEBDAV).and(osIsWindows);
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
this.resourceBundle = resourceBundle;
|
||||
|
||||
BooleanBinding isFuseOnWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.FUSE).and(osIsWindows);
|
||||
this.restrictToStableFuseOnWindows = isFuseOnWindows.and(new SimpleBooleanProperty(!environment.useExperimentalFuse())); //Is FUSE on Win and is NOT experimental fuse enabled
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -65,7 +73,10 @@ public class MountOptionsController implements FxController {
|
||||
|
||||
// readonly:
|
||||
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
|
||||
readOnlyCheckbox.disableProperty().bind(customMountFlagsCheckbox.selectedProperty());
|
||||
if(getRestrictToStableFuseOnWindows()) {
|
||||
readOnlyCheckbox.setSelected(false); // to prevent invalid states
|
||||
}
|
||||
readOnlyCheckbox.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().or(restrictToStableFuseOnWindows));
|
||||
|
||||
// custom mount flags:
|
||||
mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
|
||||
@@ -83,7 +94,7 @@ public class MountOptionsController implements FxController {
|
||||
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
|
||||
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
|
||||
|
||||
if (vault.getVaultSettings().useCustomMountPath().get()) {
|
||||
if (vault.getVaultSettings().useCustomMountPath().get() && !getRestrictToStableFuseOnWindows() /* to prevent invalid states */) {
|
||||
mountPoint.selectToggle(mountPointCustomDir);
|
||||
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
|
||||
mountPoint.selectToggle(mountPointWinDriveLetter);
|
||||
@@ -191,4 +202,8 @@ public class MountOptionsController implements FxController {
|
||||
return vault.getVaultSettings().customMountPath().get();
|
||||
}
|
||||
|
||||
public Boolean getRestrictToStableFuseOnWindows() {
|
||||
return restrictToStableFuseOnWindows.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<VBox spacing="6" HBox.hgrow="ALWAYS">
|
||||
<FormattedLabel format="%unlock.error.invalidMountPoint" arg1="${controller.mountPoint}" wrapText="true"/>
|
||||
<FormattedLabel visible="${controller.mustExist}" managed="${controller.mustExist}" format="%unlock.error.invalidMountPoint.notExisting" arg1="${controller.mountPoint}" wrapText="true"/>
|
||||
<FormattedLabel visible="${!controller.mustExist}" managed="${!controller.mustExist}" format="%unlock.error.invalidMountPoint.existing" arg1="${controller.mountPoint}" wrapText="true"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointWinDriveLetter.selected}"/>
|
||||
</HBox>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT" visible="${!controller.webDavAndWindows}" managed="${!controller.webDavAndWindows}">
|
||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom"/>
|
||||
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom" disable="${controller.restrictToStableFuseOnWindows}"/>
|
||||
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointCustomDir.selected}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
|
||||
|
||||
@@ -103,7 +103,8 @@ unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
|
||||
unlock.success.rememberChoice=Remember choice, don't show this again
|
||||
unlock.success.revealBtn=Reveal Vault
|
||||
## Invalid Mount Point
|
||||
unlock.error.invalidMountPoint=Mount point is not an empty directory: %s
|
||||
unlock.error.invalidMountPoint.notExisting=Mount point is not an empty directory or doesn't exist: %s
|
||||
unlock.error.invalidMountPoint.existing=Mount point/folder already exists or parent folder is missing: %s
|
||||
|
||||
# Migration
|
||||
migration.title=Upgrade Vault
|
||||
|
||||
@@ -99,7 +99,8 @@ unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du auf deinen
|
||||
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
|
||||
unlock.success.revealBtn=Tresor anzeigen
|
||||
## Invalid Mount Point
|
||||
unlock.error.invalidMountPoint=Einhängepunkt ist kein leeres Verzeichnis: %s
|
||||
unlock.error.invalidMountPoint.notExisting=Einhängepunkt ist kein leeres Verzeichnis oder existiert nicht: %s
|
||||
unlock.error.invalidMountPoint.existing=Einhängepunkt/-ordner existiert bereits oder das darüber liegende Verzeichnis existiert nicht: %s
|
||||
|
||||
# Migration
|
||||
migration.title=Tresor aktualisieren
|
||||
|
||||
Reference in New Issue
Block a user