/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.frontend.webdav.mount.MountParams;
import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
import org.cryptomator.frontend.webdav.mount.Mounter.Mount;
import org.cryptomator.frontend.webdav.mount.Mounter.UnmountOperation;
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
import org.cryptomator.ui.model.VaultModule.PerVault;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Binding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@PerVault
public class Vault {
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
private final Settings settings;
private final VaultSettings vaultSettings;
private final WebDavServer server;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final BooleanProperty mounted = new SimpleBooleanProperty();
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
private WebDavServletController servlet;
private Mount mount;
@Inject
Vault(Settings settings, VaultSettings vaultSettings, WebDavServer server) {
this.settings = settings;
this.vaultSettings = vaultSettings;
this.server = server;
}
// ******************************************************************************
// Commands
// ********************************************************************************/
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class);
}
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
}
public void create(CharSequence passphrase) throws IOException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(getPath())) {
for (Path file : stream) {
if (!file.getFileName().toString().startsWith(".")) {
throw new DirectoryNotEmptyException(getPath().toString());
}
}
}
if (!isValidVaultDirectory()) {
createCryptoFileSystem(passphrase).close(); // implicitly creates a non-existing vault
} 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) {
try {
FileSystem fs = getCryptoFileSystem(passphrase);
if (!server.isRunning()) {
server.start();
}
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + vaultSettings.mountName().get());
servlet.start();
Platform.runLater(() -> {
unlocked.set(true);
});
} catch (IOException e) {
LOG.error("Unable to provide filesystem", e);
}
}
public synchronized void mount() throws CommandFailedException {
if (servlet == null) {
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
}
MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get()) //
.build();
mount = servlet.mount(mountParams);
Platform.runLater(() -> {
mounted.set(true);
});
}
public synchronized void unmount() throws CommandFailedException {
unmount(Function.identity());
}
public synchronized void unmountForced() throws CommandFailedException {
unmount(Optionals.unwrap(Mount::forced));
}
private synchronized void unmount(Function<Mount, ? extends UnmountOperation> unmountOperationChooser) throws CommandFailedException {
if (mount != null) {
unmountOperationChooser.apply(mount).unmount();
}
Platform.runLater(() -> {
mounted.set(false);
});
}
public boolean supportsForcedUnmount() {
return mount != null && mount.forced().isPresent();
}
public synchronized void lock() throws Exception {
if (servlet != null) {
servlet.stop();
}
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
if (fs != null) {
fs.close();
}
Platform.runLater(() -> {
unlocked.set(false);
});
}
/**
* Ejects any mounted drives and locks this vault. no-op if this vault is currently locked.
*/
public void prepareForShutdown() {
try {
unmount();
} catch (CommandFailedException e) {
if (supportsForcedUnmount()) {
try {
unmountForced();
} catch (CommandFailedException e1) {
LOG.warn("Failed to force unmount vault.");
}
} else {
LOG.warn("Failed to gracefully unmount vault.");
}
}
try {
lock();
} catch (Exception e) {
LOG.warn("Failed to lock vault.");
}
}
public void reveal() throws CommandFailedException {
if (mount != null) {
mount.reveal();
}
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
public Observable[] observables() {
return new Observable[] {unlocked, mounted};
}
public VaultSettings getVaultSettings() {
return vaultSettings;
}
public String getWebDavUrl() {
return servlet.getServletRootUri().toString();
}
public Path getPath() {
return vaultSettings.path().getValue();
}
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
*/
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 BooleanProperty unlockedProperty() {
return unlocked;
}
public BooleanProperty mountedProperty() {
return mounted;
}
public boolean isUnlocked() {
return unlocked.get();
}
public boolean isMounted() {
return mounted.get();
}
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 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 Character getWinDriveLetter() {
if (vaultSettings.winDriveLetter().get() == null) {
return null;
} else {
return vaultSettings.winDriveLetter().get().charAt(0);
}
}
public void setWinDriveLetter(Character winDriveLetter) {
if (winDriveLetter == null) {
vaultSettings.winDriveLetter().set(null);
} else {
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;
}
}
}