mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-19 22:44:32 +00:00
Moved various non-ui related stuff to commons
This commit is contained in:
@@ -5,22 +5,87 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultList;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@Module(subcomponents = {VaultComponent.class})
|
||||
public abstract class CommonsModule {
|
||||
|
||||
@Module
|
||||
public class CommonsModule {
|
||||
private static final int NUM_SCHEDULER_THREADS = 4;
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
Comparator<String> providesSemVerComparator() {
|
||||
static Comparator<String> providesSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Settings provideSettings(SettingsProvider settingsProvider) {
|
||||
return settingsProvider.get();
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract ObservableList<Vault> bindVaultList(VaultList vaultList);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
|
||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("Background Thread " + threadNumber.getAndIncrement());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
shutdownTaskScheduler.accept(executorService::shutdown);
|
||||
return executorService;
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract ExecutorService bindExecutorService(ScheduledExecutorService executor);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
|
||||
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
|
||||
}, settings.port());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
|
||||
WebDavServer server = WebDavServer.create();
|
||||
// no need to unsubscribe eventually, because server is a singleton
|
||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -38,10 +37,11 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
public class SettingsProvider implements Provider<Settings> {
|
||||
public class SettingsProvider implements Supplier<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
|
||||
private static final long SAVE_DELAY_MS = 1000;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface DefaultMountFlags {
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
import org.cryptomator.frontend.dokany.MountFailedException;
|
||||
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 java.util.concurrent.ExecutorService;
|
||||
|
||||
public class DokanyVolume implements Volume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
||||
|
||||
private static final String FS_TYPE_NAME = "Cryptomator File System";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountFactory mountFactory;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.mountFactory = new MountFactory(executorService);
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
|
||||
Path mountPath = getMountPoint();
|
||||
String mountName = vaultSettings.mountName().get();
|
||||
try {
|
||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags.strip());
|
||||
} catch (MountFailedException e) {
|
||||
if (vaultSettings.getIndividualMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath);
|
||||
}
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path getMountPoint() throws VolumeException, IOException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
||||
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 windowsDriveLetters.getAvailableDriveLetters().iterator().next();
|
||||
} else {
|
||||
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();
|
||||
if (!success) {
|
||||
throw new VolumeException("Reveal failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() {
|
||||
mount.close();
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return MountFactory.isApplicable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
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.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||
import org.cryptomator.frontend.fuse.mount.Mounter;
|
||||
import 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;
|
||||
|
||||
public class FuseVolume implements Volume {
|
||||
|
||||
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;
|
||||
|
||||
@Inject
|
||||
public FuseVolume(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
|
||||
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] splitFlags(String str) {
|
||||
return Splitter.on(' ').splitToList(str).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.revealInFileManager();
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmountForced();
|
||||
fuseMnt.close();
|
||||
} catch (CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupTemporaryMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
fuseMnt.unmount();
|
||||
fuseMnt.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return FuseVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface PerVault {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@PerVault
|
||||
public class Vault {
|
||||
|
||||
public static final Predicate<Vault> NOT_LOCKED = hasState(State.LOCKED).negate();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Provider<Volume> volumeProvider;
|
||||
private final Supplier<String> defaultMountFlags;
|
||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<State>(State.LOCKED);
|
||||
private final StringBinding displayableName;
|
||||
private final StringBinding displayablePath;
|
||||
private final BooleanBinding locked;
|
||||
private final BooleanBinding unlocked;
|
||||
|
||||
private Volume volume;
|
||||
|
||||
public enum State {
|
||||
LOCKED, PROCESSING, UNLOCKED
|
||||
}
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags Supplier<String> defaultMountFlags) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
|
||||
this.displayableName = Bindings.createStringBinding(this::getDisplayableName, vaultSettings.path());
|
||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||
this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Commands
|
||||
// ********************************************************************************/
|
||||
|
||||
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
|
||||
}
|
||||
|
||||
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
List<FileSystemFlags> flags = new ArrayList<>();
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.add(FileSystemFlags.READONLY);
|
||||
}
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withFlags(flags) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
|
||||
public void create(CharSequence passphrase) throws IOException {
|
||||
if (!isValidVaultDirectory()) {
|
||||
CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
|
||||
} else {
|
||||
throw new FileAlreadyExistsException(getPath().toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
|
||||
CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
|
||||
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
|
||||
throw new NotDirectoryException("");
|
||||
}
|
||||
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getMountFlags());
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws Volume.VolumeException {
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
|
||||
if (fs != null) {
|
||||
try {
|
||||
fs.close();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error closing file system.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejects any mounted drives and locks this vault. no-op if this vault is currently locked.
|
||||
*/
|
||||
public void prepareForShutdown() {
|
||||
try {
|
||||
lock(false);
|
||||
} catch (Volume.VolumeException e) {
|
||||
if (volume.supportsForcedUnmount()) {
|
||||
try {
|
||||
lock(true);
|
||||
} catch (Volume.VolumeException e1) {
|
||||
LOG.warn("Failed to force lock vault.", e1);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Failed to gracefully lock vault.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal() throws Volume.VolumeException {
|
||||
volume.reveal();
|
||||
}
|
||||
|
||||
public static Predicate<Vault> hasState(State state) {
|
||||
return vault -> {
|
||||
return vault.getState() == state;
|
||||
};
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Observable Properties
|
||||
// *******************************************************************************
|
||||
|
||||
public ObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
public void setState(State value) {
|
||||
state.setValue(value);
|
||||
}
|
||||
|
||||
public BooleanBinding lockedProperty() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return state.get() == State.LOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding unlockedProperty() {
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return state.get() == State.UNLOCKED;
|
||||
}
|
||||
|
||||
public StringBinding displayableNameProperty() {
|
||||
return displayableName;
|
||||
}
|
||||
|
||||
public String getDisplayableName() {
|
||||
Path p = vaultSettings.path().get();
|
||||
return p.getFileName().toString();
|
||||
}
|
||||
|
||||
public StringBinding displayablePathProperty() {
|
||||
return displayablePath;
|
||||
}
|
||||
|
||||
public String getDisplayablePath() {
|
||||
Path p = vaultSettings.path().get();
|
||||
if (p.startsWith(HOME_DIR)) {
|
||||
Path relativePath = HOME_DIR.relativize(p);
|
||||
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
||||
return homePrefix + relativePath.toString();
|
||||
} else {
|
||||
return p.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Getter/Setter
|
||||
// *******************************************************************************/
|
||||
|
||||
public Observable[] observables() {
|
||||
return new Observable[]{state};
|
||||
}
|
||||
|
||||
public VaultSettings getVaultSettings() {
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return vaultSettings.path().getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use displayablePathProperty() instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.5.0")
|
||||
public Binding<String> displayablePath() {
|
||||
Path homeDir = Paths.get(SystemUtils.USER_HOME);
|
||||
return EasyBind.map(vaultSettings.path(), p -> {
|
||||
if (p.startsWith(homeDir)) {
|
||||
Path relativePath = homeDir.relativize(p);
|
||||
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
||||
return homePrefix + relativePath.toString();
|
||||
} else {
|
||||
return p.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directory name without preceeding path components and file extension
|
||||
* @deprecated use nameProperty() instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.5.0")
|
||||
public Binding<String> name() {
|
||||
return EasyBind.map(vaultSettings.path(), Path::getFileName).map(Path::toString);
|
||||
}
|
||||
|
||||
public boolean doesVaultDirectoryExist() {
|
||||
return Files.isDirectory(getPath());
|
||||
}
|
||||
|
||||
public boolean isValidVaultDirectory() {
|
||||
return CryptoFileSystemProvider.containsVault(getPath(), MASTERKEY_FILENAME);
|
||||
}
|
||||
|
||||
public long pollBytesRead() {
|
||||
CryptoFileSystem fs = cryptoFileSystem.get();
|
||||
if (fs != null) {
|
||||
return fs.getStats().pollBytesRead();
|
||||
} else {
|
||||
return 0l;
|
||||
}
|
||||
}
|
||||
|
||||
public long pollBytesWritten() {
|
||||
CryptoFileSystem fs = cryptoFileSystem.get();
|
||||
if (fs != null) {
|
||||
return fs.getStats().pollBytesWritten();
|
||||
} else {
|
||||
return 0l;
|
||||
}
|
||||
}
|
||||
|
||||
public String getCustomMountPath() {
|
||||
return vaultSettings.individualMountPath().getValueSafe();
|
||||
}
|
||||
|
||||
public void setCustomMountPath(String mountPath) {
|
||||
vaultSettings.individualMountPath().set(mountPath);
|
||||
}
|
||||
|
||||
public String getMountName() {
|
||||
return vaultSettings.mountName().get();
|
||||
}
|
||||
|
||||
public void setMountName(String mountName) throws IllegalArgumentException {
|
||||
if (StringUtils.isBlank(mountName)) {
|
||||
throw new IllegalArgumentException("mount name is empty");
|
||||
} else {
|
||||
vaultSettings.mountName().set(VaultSettings.normalizeMountName(mountName));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHavingCustomMountFlags() {
|
||||
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
||||
}
|
||||
|
||||
public String getMountFlags() {
|
||||
String mountFlags = vaultSettings.mountFlags().get();
|
||||
if (Strings.isNullOrEmpty(mountFlags)) {
|
||||
return defaultMountFlags.get();
|
||||
} else {
|
||||
return mountFlags;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMountFlags(String mountFlags) {
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
}
|
||||
|
||||
public Character getWinDriveLetter() {
|
||||
if (vaultSettings.winDriveLetter().get() == null) {
|
||||
return null;
|
||||
} else {
|
||||
return vaultSettings.winDriveLetter().get().charAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setWinDriveLetter(Path root) {
|
||||
if (root == null) {
|
||||
vaultSettings.winDriveLetter().set(null);
|
||||
} else {
|
||||
char winDriveLetter = root.toString().charAt(0);
|
||||
vaultSettings.winDriveLetter().set(String.valueOf(winDriveLetter));
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return vaultSettings.getId();
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Hashcode / Equals
|
||||
// *******************************************************************************/
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(vaultSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Vault && obj.getClass().equals(this.getClass())) {
|
||||
final Vault other = (Vault) obj;
|
||||
return Objects.equals(this.vaultSettings, other.vaultSettings);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportsForcedUnmount() {
|
||||
return volume.supportsForcedUnmount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
|
||||
@PerVault
|
||||
@Subcomponent(modules = {VaultModule.class})
|
||||
public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder vaultSettings(VaultSettings vaultSettings);
|
||||
|
||||
VaultComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@Singleton
|
||||
public class VaultFactory {
|
||||
|
||||
private final VaultComponent.Builder vaultComponentBuilder;
|
||||
private final ConcurrentMap<VaultSettings, Vault> vaults = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
public VaultFactory(VaultComponent.Builder vaultComponentBuilder) {
|
||||
this.vaultComponentBuilder = vaultComponentBuilder;
|
||||
}
|
||||
|
||||
public Vault get(VaultSettings vaultSettings) {
|
||||
return vaults.computeIfAbsent(vaultSettings, this::create);
|
||||
}
|
||||
|
||||
private Vault create(VaultSettings vaultSettings) {
|
||||
VaultComponent comp = vaultComponentBuilder.vaultSettings(vaultSettings).build();
|
||||
return comp.vault();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.TransformationList;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@Singleton
|
||||
public class VaultList extends TransformationList<Vault, VaultSettings> {
|
||||
|
||||
private final VaultFactory vaultFactory;
|
||||
private final ObservableList<VaultSettings> source;
|
||||
|
||||
@Inject
|
||||
public VaultList(Settings settings, VaultFactory vaultFactory) {
|
||||
super(settings.getDirectories());
|
||||
this.source = settings.getDirectories();
|
||||
this.vaultFactory = vaultFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSourceIndex(int index) {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewIndex(int index) {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vault get(int index) {
|
||||
VaultSettings s = source.get(index);
|
||||
return vaultFactory.get(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, Vault element) {
|
||||
source.add(index, element.getVaultSettings());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vault remove(int index) {
|
||||
VaultSettings s = source.remove(index);
|
||||
return vaultFactory.get(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return getSource().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sourceChanged(ListChangeListener.Change<? extends VaultSettings> c) {
|
||||
this.fireChange(new VaultListChange(c));
|
||||
}
|
||||
|
||||
private class VaultListChange extends ListChangeListener.Change<Vault> {
|
||||
|
||||
private final ListChangeListener.Change<? extends VaultSettings> delegate;
|
||||
|
||||
public VaultListChange(ListChangeListener.Change<? extends VaultSettings> delegate) {
|
||||
super(VaultList.this);
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean next() {
|
||||
return delegate.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasUpdated() {
|
||||
return delegate.wasUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
delegate.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFrom() {
|
||||
return delegate.getFrom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTo() {
|
||||
return delegate.getTo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Vault> getRemoved() {
|
||||
return Lists.transform(delegate.getRemoved(), vaultFactory::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasPermutated() {
|
||||
return delegate.wasPermutated();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getPermutation() {
|
||||
if (delegate.wasPermutated()) {
|
||||
return IntStream.range(getFrom(), getTo()).map(delegate::getPermutation).toArray();
|
||||
} else {
|
||||
return new int[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Module
|
||||
public class VaultModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
|
||||
return dokanyVolume;
|
||||
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
|
||||
return fuseVolume;
|
||||
} else {
|
||||
if (VolumeImpl.WEBDAV != preferredImpl) {
|
||||
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
|
||||
}
|
||||
assert webDavVolume.isSupported();
|
||||
return webDavVolume;
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@DefaultMountFlags
|
||||
public Supplier<String> provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
return () -> {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
switch (preferredImpl) {
|
||||
case FUSE:
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return getMacFuseDefaultMountFlags(settings, vaultSettings);
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return getLinuxFuseDefaultMountFlags(settings, vaultSettings);
|
||||
}
|
||||
case DOKANY:
|
||||
return getDokanyDefaultMountFlags(settings, vaultSettings);
|
||||
default:
|
||||
return "--flags-supported-on-FUSE-or-DOKANY-only";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||
private String getMacFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
assert SystemUtils.IS_OS_MAC_OSX;
|
||||
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.append(" -ordonly");
|
||||
}
|
||||
flags.append(" -ovolname=").append(vaultSettings.mountName().get());
|
||||
flags.append(" -oatomic_o_trunc");
|
||||
flags.append(" -oauto_xattr");
|
||||
flags.append(" -oauto_cache");
|
||||
flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding
|
||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
|
||||
private String getLinuxFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
assert SystemUtils.IS_OS_LINUX;
|
||||
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.append(" -oro");
|
||||
}
|
||||
flags.append(" -oauto_unmount");
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
||||
private String getDokanyDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
|
||||
StringBuilder flags = new StringBuilder();
|
||||
flags.append(" --options CURRENT_SESSION");
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.append(",WRITE_PROTECTION");
|
||||
}
|
||||
flags.append(" --thread-count 5");
|
||||
flags.append(" --timeout 10000");
|
||||
flags.append(" --allocation-unit-size 4096");
|
||||
flags.append(" --sector-size 4096");
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Takes a Volume and usess it to mount an unlocked vault
|
||||
*/
|
||||
public interface Volume {
|
||||
|
||||
/**
|
||||
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
|
||||
*
|
||||
* @return true if this volume can be mounted
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException;
|
||||
|
||||
void reveal() throws VolumeException;
|
||||
|
||||
void unmount() throws VolumeException;
|
||||
|
||||
// optional forced unmounting:
|
||||
|
||||
default boolean supportsForcedUnmount() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void unmountForced() throws VolumeException {
|
||||
throw new VolumeException("Operation not supported.");
|
||||
}
|
||||
|
||||
static VolumeImpl[] getCurrentSupportedAdapters() {
|
||||
return Stream.of(VolumeImpl.values()).filter(impl -> {
|
||||
switch (impl) {
|
||||
case WEBDAV:
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
case DOKANY:
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
case FUSE:
|
||||
return FuseVolume.isSupportedStatic();
|
||||
default:
|
||||
return false;//throw new IllegalStateException("Adapter not implemented.");
|
||||
}
|
||||
}).toArray(VolumeImpl[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
|
||||
*/
|
||||
class VolumeException extends Exception {
|
||||
|
||||
public VolumeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public VolumeException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VolumeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.cryptomator.frontend.webdav.mount.MountParams;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter;
|
||||
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
|
||||
private static final String LOCALHOST_ALIAS = "cryptomator-vault";
|
||||
|
||||
private final Provider<WebDavServer> serverProvider;
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Settings settings;
|
||||
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
|
||||
this.serverProvider = serverProvider;
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
if (!server.isRunning()) {
|
||||
server.start();
|
||||
}
|
||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + vaultSettings.mountName().get());
|
||||
servlet.start();
|
||||
mount();
|
||||
}
|
||||
|
||||
private void mount() throws VolumeException {
|
||||
if (servlet == null) {
|
||||
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
||||
}
|
||||
MountParams mountParams = MountParams.create() //
|
||||
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
|
||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||
.build();
|
||||
try {
|
||||
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
e.printStackTrace();
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws VolumeException {
|
||||
try {
|
||||
mount.reveal();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
e.printStackTrace();
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.forced().orElseThrow(IllegalStateException::new).unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
try {
|
||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||
if (alias.getHostAddress().equals("127.0.0.1")) {
|
||||
return LOCALHOST_ALIAS;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (servlet != null) {
|
||||
servlet.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
}
|
||||
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Singleton
|
||||
public final class WindowsDriveLetters {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
|
||||
private static final Set<Path> D_TO_Z;
|
||||
|
||||
static {
|
||||
try (IntStream stream = IntStream.rangeClosed('D', 'Z')) {
|
||||
D_TO_Z = stream.mapToObj(i -> Path.of(((char) i)+":\\")).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
public WindowsDriveLetters() {
|
||||
}
|
||||
|
||||
public Set<Path> getOccupiedDriveLetters() {
|
||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||
LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
|
||||
return Set.of();
|
||||
} else {
|
||||
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Path> getAvailableDriveLetters() {
|
||||
Set<Path> occupiedDriveLetters = getOccupiedDriveLetters();
|
||||
Predicate<Path> isOccupiedDriveLetter = occupiedDriveLetters::contains;
|
||||
return D_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user