~exprez135/cryptomator-libre

b781cf6f2550b68f7fa414282a95b65aa68c92bf — Sebastian Stenzel 2 months ago 41492a9 + 9b653f4 1.5.10
Merge branch 'hotfix/1.5.10'
M .idea/runConfigurations/Cryptomator_macOS.xml => .idea/runConfigurations/Cryptomator_macOS.xml +1 -2
@@ 5,8 5,7 @@
    </envs>
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Xss2m -Xmx512m -ea" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Xss2m -Xmx512m -ea" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>

M .idea/runConfigurations/Cryptomator_macOS_Dev.xml => .idea/runConfigurations/Cryptomator_macOS_Dev.xml +1 -1
@@ 5,7 5,7 @@
    </envs>
    <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
    <module name="launcher" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Dfuse.experimental=&quot;true&quot; -Xss2m -Xmx512m -ea" />
    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Xss2m -Xmx512m -ea" />
    <method v="2">
      <option name="Make" enabled="true" />
    </method>

M main/buildkit/pom.xml => main/buildkit/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.9</version>
		<version>1.5.10</version>
	</parent>
	<artifactId>buildkit</artifactId>
	<packaging>pom</packaging>

M main/commons/pom.xml => main/commons/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.9</version>
		<version>1.5.10</version>
	</parent>
	<artifactId>commons</artifactId>
	<name>Cryptomator Commons</name>

M main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/AvailableDriveLetterChooser.java +1 -8
@@ 8,9 8,7 @@ import javax.inject.Inject;
import java.nio.file.Path;
import java.util.Optional;

public class AvailableDriveLetterChooser implements MountPointChooser {

	public static final int PRIORITY = 200;
class AvailableDriveLetterChooser implements MountPointChooser {

	private final WindowsDriveLetters windowsDriveLetters;



@@ 28,9 26,4 @@ public class AvailableDriveLetterChooser implements MountPointChooser {
	public Optional<Path> chooseMountPoint(Volume caller) {
		return this.windowsDriveLetters.getAvailableDriveLetterPath();
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomDriveLetterChooser.java +1 -8
@@ 9,9 9,7 @@ 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;
class CustomDriveLetterChooser implements MountPointChooser {

	private final VaultSettings vaultSettings;



@@ 29,9 27,4 @@ public class CustomDriveLetterChooser implements MountPointChooser {
	public Optional<Path> chooseMountPoint(Volume caller) {
		return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +1 -7
@@ 20,9 20,7 @@ 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;
class CustomMountPointChooser implements MountPointChooser {

	private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);



@@ 94,8 92,4 @@ public class CustomMountPointChooser implements MountPointChooser {
		}
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java +30 -5
@@ 1,20 1,44 @@
package org.cryptomator.common.mountpoint;

import org.cryptomator.common.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
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;

public class IrregularUnmountCleaner {
@Singleton
class IrregularUnmountCleaner {

	public static Logger LOG = LoggerFactory.getLogger(IrregularUnmountCleaner.class);

	public static void removeIrregularUnmountDebris(Path dirContainingMountPoints) {
	private final Optional<Path> tmpMountPointDir;
	private volatile boolean alreadyChecked = false;

	@Inject
	public IrregularUnmountCleaner(Environment env) {
		this.tmpMountPointDir = env.getMountPointsDir();
	}


	public synchronized void clearIrregularUnmountDebrisIfNeeded() {
		if (alreadyChecked || tmpMountPointDir.isEmpty()) {
			return; //nuthin to do
		}
		if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
			clearIrregularUnmountDebris(tmpMountPointDir.get());
		}
		alreadyChecked = true;
	}

	private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
		IOException cleanupFailed = new IOException("Cleanup failed");

		try {


@@ 41,11 65,12 @@ public class IrregularUnmountCleaner {
			}
		} catch (IOException e) {
			LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
		} finally {
			alreadyChecked = true;
		}

	}

	private static void deleteEmptyDir(Path dir) throws IOException {
	private void deleteEmptyDir(Path dir) throws IOException {
		assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
		try {
			Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)


@@ 54,7 79,7 @@ public class IrregularUnmountCleaner {
		}
	}

	private static void deleteDeadLink(Path symlink) throws IOException {
	private void deleteDeadLink(Path symlink) throws IOException {
		assert Files.isSymbolicLink(symlink);
		if (Files.notExists(symlink)) { // following link: target does not exist
			Files.delete(symlink);

A main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java +64 -0
@@ 0,0 1,64 @@
package org.cryptomator.common.mountpoint;

import org.apache.commons.lang3.SystemUtils;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

class MacVolumeMountChooser implements MountPointChooser {

	private static final Logger LOG = LoggerFactory.getLogger(MacVolumeMountChooser.class);
	private static final int MAX_MOUNTPOINT_CREATION_RETRIES = 10;
	private static final Path VOLUME_PATH = Path.of("/Volumes");

	private final VaultSettings vaultSettings;

	@Inject
	public MacVolumeMountChooser(VaultSettings vaultSettings) {
		this.vaultSettings = vaultSettings;
	}

	@Override
	public boolean isApplicable(Volume caller) {
		return SystemUtils.IS_OS_MAC;
	}

	@Override
	public Optional<Path> chooseMountPoint(Volume caller) {
		String basename = this.vaultSettings.mountName().get();
		// regular
		Path mountPoint = VOLUME_PATH.resolve(basename);
		if (Files.notExists(mountPoint)) {
			return Optional.of(mountPoint);
		}
		// with id
		mountPoint = VOLUME_PATH.resolve(basename + " (" + vaultSettings.getId() + ")");
		if (Files.notExists(mountPoint)) {
			return Optional.of(mountPoint);
		}
		// with id and count
		for (int i = 1; i < MAX_MOUNTPOINT_CREATION_RETRIES; i++) {
			mountPoint = VOLUME_PATH.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
			if (Files.notExists(mountPoint)) {
				return Optional.of(mountPoint);
			}
		}
		LOG.error("Failed to find feasible mountpoint at /Volumes/{}_x. Giving up after {} attempts.", basename, MAX_MOUNTPOINT_CREATION_RETRIES);
		return Optional.empty();
	}

	@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;
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java +1 -32
@@ 47,7 47,7 @@ import java.util.SortedSet;
 * If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
 * remaining cleanup.
 */
public interface MountPointChooser extends Comparable<MountPointChooser> {
public interface MountPointChooser {

	/**
	 * Called by the {@link Volume} to determine whether this MountPointChooser is


@@ 135,35 135,4 @@ public interface MountPointChooser extends Comparable<MountPointChooser> {
		//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());
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java +23 -11
@@ 1,15 1,17 @@
package org.cryptomator.common.mountpoint;

import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import dagger.multibindings.IntKey;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.PerVault;

import javax.inject.Named;
import java.util.Set;
import java.util.SortedSet;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>


@@ 21,30 23,40 @@ import java.util.SortedSet;
public abstract class MountPointChooserModule {

	@Binds
	@IntoSet
	@IntoMap
	@IntKey(0)
	@PerVault
	public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);

	@Binds
	@IntoSet
	@IntoMap
	@IntKey(100)
	@PerVault
	public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);

	@Binds
	@IntoSet
	@IntoMap
	@IntKey(101)
	@PerVault
	public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);

	@Binds
	@IntoMap
	@IntKey(200)
	@PerVault
	public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);

	@Binds
	@IntoSet
	@IntoMap
	@IntKey(999)
	@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);
	public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
		SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(choosers);
		return Iterables.unmodifiableIterable(sortedChoosers.values());
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java +12 -19
@@ 1,6 1,5 @@
package org.cryptomator.common.mountpoint;

import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;


@@ 12,23 11,23 @@ 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;
class TemporaryMountPointChooser implements MountPointChooser {

	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;
	private final IrregularUnmountCleaner cleaner;
	private volatile boolean clearedDebris;

	@Inject
	public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment) {
	public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, IrregularUnmountCleaner cleaner) {
		this.vaultSettings = vaultSettings;
		this.environment = environment;
		this.cleaner = cleaner;
	}

	@Override


@@ 42,9 41,14 @@ public class TemporaryMountPointChooser implements MountPointChooser {

	@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
		cleaner.clearIrregularUnmountDebrisIfNeeded();
		return this.environment.getMountPointsDir().map(this::choose);
	}


	private Path choose(Path parent) {
		String basename = this.vaultSettings.mountName().get();
		//regular


@@ 53,13 57,13 @@ public class TemporaryMountPointChooser implements MountPointChooser {
			return mountPoint;
		}
		//with id
		mountPoint = parent.resolve(basename + " (" +vaultSettings.getId() + ")");
		mountPoint = parent.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 = parent.resolve(basename + "_(" +vaultSettings.getId() + ")_"+i);
			mountPoint = parent.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
			if (Files.notExists(mountPoint)) {
				return mountPoint;
			}


@@ 70,13 74,6 @@ public class TemporaryMountPointChooser implements MountPointChooser {

	@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 -> {


@@ 114,8 111,4 @@ public class TemporaryMountPointChooser implements MountPointChooser {
		}
	}

	@Override
	public int getPriority() {
		return PRIORITY;
	}
}

M main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java +7 -22
@@ 1,50 1,35 @@
package org.cryptomator.common.vaults;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
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;
import java.util.SortedSet;
import java.util.TreeSet;

public abstract class AbstractVolume implements Volume {

	private final SortedSet<MountPointChooser> choosers;
	private final Iterable<MountPointChooser> choosers;

	protected Path mountPoint;

	//Cleanup
	private boolean cleanupRequired;
	private MountPointChooser usedChooser;

	public AbstractVolume(SortedSet<MountPointChooser> choosers) {
	public AbstractVolume(Iterable<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;
			}

		for (var chooser : Iterables.filter(choosers, c -> c.isApplicable(this))) {
			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
			if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
				continue;
			}
			this.cleanupRequired = chooser.prepare(this, chosenPath.get()); //Fail entirely if an Exception occurs
			this.cleanupRequired = chooser.prepare(this, chosenPath.get());
			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));
		throw new InvalidMountPointException("No feasible MountPoint found!");
	}

	protected void cleanupMountPoint() {

M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +1 -1
@@ 28,7 28,7 @@ public class DokanyVolume extends AbstractVolume {
	private Mount mount;

	@Inject
	public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") SortedSet<MountPointChooser> choosers) {
	public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
		super(choosers);
		this.vaultSettings = vaultSettings;
		this.mountFactory = new MountFactory(executorService);

M main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java +21 -3
@@ 1,6 1,6 @@
package org.cryptomator.common.vaults;

import com.google.common.base.Splitter;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;


@@ 18,16 18,20 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.regex.Pattern;

public class FuseVolume extends AbstractVolume {

	private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
	private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532

	private Mount mount;

	@Inject
	public FuseVolume(@Named("orderedMountPointChoosers") SortedSet<MountPointChooser> choosers) {
	public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
		super(choosers);
	}



@@ 51,7 55,21 @@ public class FuseVolume extends AbstractVolume {
	}

	private String[] splitFlags(String str) {
		return Splitter.on(' ').splitToList(str).toArray(String[]::new);
		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

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +2 -2
@@ 101,7 101,7 @@ public class VaultModule {
		if (readOnly.get()) {
			flags.append(" -ordonly");
		}
		flags.append(" -ovolname=").append(mountName.get());
		flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
		flags.append(" -oatomic_o_trunc");
		flags.append(" -oauto_xattr");
		flags.append(" -oauto_cache");


@@ 158,7 158,7 @@ public class VaultModule {
			flags.append(" -ouid=-1");
			flags.append(" -ogid=-1");
		}
		flags.append(" -ovolname=").append(mountName.get());
		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);

M main/launcher/pom.xml => main/launcher/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.9</version>
		<version>1.5.10</version>
	</parent>
	<artifactId>launcher</artifactId>
	<name>Cryptomator Launcher</name>

M main/pom.xml => main/pom.xml +1 -1
@@ 3,7 3,7 @@
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.cryptomator</groupId>
	<artifactId>main</artifactId>
	<version>1.5.9</version>
	<version>1.5.10</version>
	<packaging>pom</packaging>
	<name>Cryptomator</name>


M main/ui/pom.xml => main/ui/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.9</version>
		<version>1.5.10</version>
	</parent>
	<artifactId>ui</artifactId>
	<name>Cryptomator GUI</name>

M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java => main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java +1 -11
@@ 1,7 1,5 @@
package org.cryptomator.ui.launcher;

import org.cryptomator.common.Environment;
import org.cryptomator.common.mountpoint.IrregularUnmountCleaner;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;


@@ 16,8 14,6 @@ import javafx.collections.ObservableList;
import java.awt.Desktop;
import java.awt.SystemTray;
import java.awt.desktop.AppReopenedListener;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.util.Collection;
import java.util.Optional;



@@ 32,17 28,15 @@ public class UiLauncher {
	private final FxApplicationStarter fxApplicationStarter;
	private final AppLaunchEventHandler launchEventHandler;
	private final Optional<TrayIntegrationProvider> trayIntegration;
	private final Environment env;

	@Inject
	public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration, Environment env) {
	public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) {
		this.settings = settings;
		this.vaults = vaults;
		this.trayComponent = trayComponent;
		this.fxApplicationStarter = fxApplicationStarter;
		this.launchEventHandler = launchEventHandler;
		this.trayIntegration = trayIntegration;
		this.env = env;
	}

	public void launch() {


@@ 65,10 59,6 @@ public class UiLauncher {
		// register app reopen listener
		Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));

		//clean leftovers of not-regularly unmounted vaults
		//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
		env.getMountPointsDir().filter(path -> Files.exists(path, LinkOption.NOFOLLOW_LINKS)).ifPresent(IrregularUnmountCleaner::removeIrregularUnmountDebris);

		// auto unlock
		Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
		if (!vaultsToAutoUnlock.isEmpty()) {

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +9 -4
@@ 31,6 31,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.Optional;


@@ 78,7 79,7 @@ public class UnlockController implements FxController {
		this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
		this.unlockButtonDisabled = new SimpleBooleanProperty();
		this.vaultName = WeakBindings.bindString(vault.displayNameProperty());
		this.window.setOnCloseRequest(windowEvent -> cancel());
		this.window.setOnHiding(this::windowClosed);
	}

	@FXML


@@ 128,14 129,18 @@ public class UnlockController implements FxController {
		passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
	}


	@FXML
	public void cancel() {
		LOG.debug("Unlock canceled by user.");
		window.close();
		passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
	}

	private void windowClosed(WindowEvent windowEvent) {
		// if not already interacted, mark this workflow as cancelled:
		if (passwordEntryLock.awaitingInteraction().get()) {
			LOG.debug("Unlock canceled by user.");
			passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
		}
	}

	@FXML
	public void unlock() {