call me THE DESTROYER!!!

first compile-clean but totally fubar version
This commit is contained in:
Sebastian Stenzel
2016-12-16 17:20:56 +01:00
parent 28fedafb59
commit 79b825aaad
311 changed files with 675 additions and 25029 deletions
+8 -28
View File
@@ -24,35 +24,11 @@
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
<artifactId>cryptofs</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nio</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nameshortening</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-stats</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-webdav</artifactId>
<artifactId>webdav-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
@@ -77,8 +53,8 @@
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Guava -->
@@ -96,6 +72,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
@@ -15,15 +15,15 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.ApplicationVersion;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,7 +33,7 @@ public class Cryptomator {
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
public static void main(String[] args) {
LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
@@ -91,7 +91,7 @@ public class Cryptomator {
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
SHUTDOWN_TASKS.put(r, Boolean.TRUE);
}
public static void removeShutdownTask(Runnable r) {
@@ -102,7 +102,7 @@ public class Cryptomator {
@Override
public void run() {
LOG.debug("Shutting down");
SHUTDOWN_TASKS.forEach(r -> {
SHUTDOWN_TASKS.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
@@ -8,24 +8,20 @@
*******************************************************************************/
package org.cryptomator.ui;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
import org.cryptomator.ui.model.VaultComponent;
import org.cryptomator.ui.model.VaultModule;
import org.cryptomator.ui.util.DeferredCloser;
import dagger.Component;
@Singleton
@Component(modules = CryptomatorModule.class)
interface CryptomatorComponent {
AsyncTaskService asyncTaskService();
public interface CryptomatorComponent {
ExecutorService executorService();
@@ -33,12 +29,10 @@ interface CryptomatorComponent {
MainController mainController();
Localization localization();
ExitUtil exitUtil();
DebugMode debugMode();
Optional<MacFunctions> nativeMacFunctions();
VaultComponent newVaultComponent(VaultModule vaultModule);
}
@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.ui;
import static java.util.stream.Collectors.toList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -17,32 +15,22 @@ import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
import org.cryptomator.cryptolib.CryptoLibModule;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.frontend.webdav.WebDavModule;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.jni.JniModule;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.model.Vaults;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.stage.Stage;
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class, CryptoLibModule.class})
@Module(includes = {CommonsModule.class, KeychainModule.class, JniModule.class, CryptoLibModule.class})
class CryptomatorModule {
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class);
@@ -81,13 +69,6 @@ class CryptomatorModule {
return closer;
}
@Provides
@Singleton
@Named("VaultJsonMapper")
ObjectMapper provideVaultObjectMapper(VaultObjectMapperProvider vaultObjectMapperProvider) {
return vaultObjectMapperProvider.get();
}
@Provides
@Singleton
Settings provideSettings(SettingsProvider settingsProvider) {
@@ -102,17 +83,8 @@ class CryptomatorModule {
@Provides
@Singleton
FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Vaults vaults, Settings settings) {
vaults.addListener((Observable o) -> setValidFrontendIds(webDavServer, vaults));
setValidFrontendIds(webDavServer, vaults);
webDavServer.setPort(settings.getPort());
webDavServer.start();
return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new);
}
private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) {
webDavServer.setValidFrontendIds(vaults.stream() //
.map(Vault::getId).map(FrontendId::from).collect(toList()));
WebDavServer provideWebDavServer(Settings settings) {
return WebDavServer.create("localhost", settings.getPort());
}
}
@@ -18,8 +18,8 @@ import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
@@ -29,9 +29,10 @@ import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.Vaults;
import org.cryptomator.ui.model.VaultList;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
@@ -79,7 +80,7 @@ public class MainController extends LocalizedFXMLViewController {
private final Lazy<SettingsController> settingsController;
private final Lazy<UpgradeStrategies> upgradeStrategies;
private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
private final Vaults vaults;
private final VaultList vaults;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final BooleanExpression isSelectedVaultUnlocked = BooleanBinding.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false));
private final BooleanExpression isSelectedVaultValid = BooleanBinding.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
@@ -91,7 +92,7 @@ public class MainController extends LocalizedFXMLViewController {
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies,
Vaults vaults) {
VaultList vaults) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -230,7 +231,9 @@ public class MainController extends LocalizedFXMLViewController {
return;
}
final Vault vault = vaultFactoy.createVault(vaultPath);
final VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.setPath(vaultPath);
final Vault vault = vaultFactoy.get(vaultSettings);
if (!vaults.contains(vault)) {
vaults.add(vault);
}
@@ -18,15 +18,13 @@ import javax.inject.Inject;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.frontend.webdav.ServerLifecycleException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.WindowsDriveLetters;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
@@ -34,7 +32,6 @@ import org.cryptomator.ui.util.DialogBuilderUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
@@ -61,7 +58,6 @@ public class UnlockController extends LocalizedFXMLViewController {
private final Application app;
private final AsyncTaskService asyncTaskService;
private final Lazy<FrontendFactory> frontendFactory;
private final Settings settings;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
@@ -70,12 +66,10 @@ public class UnlockController extends LocalizedFXMLViewController {
private Optional<UnlockListener> listener = Optional.empty();
@Inject
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters,
Optional<KeychainAccess> keychainAccess) {
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Settings settings, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess) {
super(localization);
this.app = app;
this.asyncTaskService = asyncTaskService;
this.frontendFactory = frontendFactory;
this.settings = settings;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
@@ -326,7 +320,7 @@ public class UnlockController extends LocalizedFXMLViewController {
private void unlock(CharSequence password) {
try {
vault.activateFrontend(frontendFactory.get(), settings, password);
vault.activateFrontend(password);
vault.reveal();
Platform.runLater(() -> {
messageText.setText(null);
@@ -356,8 +350,8 @@ public class UnlockController extends LocalizedFXMLViewController {
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
}
});
} catch (FrontendCreationFailedException | CommandFailedException e) {
LOG.error("Decryption failed for technical reasons.", e);
} catch (ServerLifecycleException e) {
LOG.error("Unlock failed for technical reasons.", e);
Platform.runLater(() -> {
messageText.setText(localization.getString("unlock.errorMessage.mountingFailed"));
});
@@ -14,7 +14,6 @@ import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Provider;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
@@ -107,13 +106,16 @@ public class UnlockedController extends LocalizedFXMLViewController {
}
// listen to MAC warnings as long as this vault is unlocked:
final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
newVault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
newVault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
if (Boolean.FALSE.equals(newValue)) {
newVault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
}
});
// TODO overheadhunter: reimplement eventually
/*
* final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
* newVault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
* newVault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
* if (Boolean.FALSE.equals(newValue)) {
* newVault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
* }
* });
*/
if (!vault.get().isMounted()) {
// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
@@ -150,7 +152,8 @@ public class UnlockedController extends LocalizedFXMLViewController {
private void didClickRevealVault(ActionEvent event) {
asyncTaskService.asyncTaskOf(() -> {
vault.get().reveal();
}).onError(CommandFailedException.class, () -> {
}).onError(RuntimeException.class, () -> {
// TODO overheadhunter catch more specific exception type thrown by reveal()
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
}).run();
}
@@ -116,7 +116,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
asyncTaskService //
.asyncTaskOf(() -> {
if (!instruction.isApplicable(vault)) {
throw new IllegalStateException("No ugprade needed for " + vault.path().getValue());
throw new IllegalStateException("No ugprade needed for " + vault.getPath());
}
instruction.upgrade(vault, passwordField.getCharacters());
}) //
@@ -9,9 +9,11 @@
package org.cryptomator.ui.controllers;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
@@ -24,7 +26,6 @@ import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
@@ -33,8 +34,9 @@ import org.cryptomator.ui.util.AsyncTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import javafx.application.Application;
import javafx.application.Platform;
@@ -110,10 +112,10 @@ public class WelcomeController extends LocalizedFXMLViewController {
client.executeMethod(method);
final InputStream responseBodyStream = method.getResponseBodyAsStream();
if (method.getStatusCode() == HttpStatus.SC_OK && responseBodyStream != null) {
final byte[] responseData = IOUtils.toByteArray(responseBodyStream);
final ObjectMapper mapper = new ObjectMapper();
final Map<String, String> map = mapper.readValue(responseData, new TypeReference<HashMap<String, String>>() {
});
Gson gson = new GsonBuilder().setLenient().create();
Reader utf8Reader = new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8);
Map<String, String> map = gson.fromJson(utf8Reader, new TypeToken<Map<String, String>>() {
}.getType());
if (map != null) {
this.compareVersions(map);
}
@@ -59,7 +59,7 @@ public class DirectoryListCell extends DraggableListCell<Vault> {
statusIndicator.fillProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).filter(Boolean.TRUE::equals).map(unlocked -> GREEN_FILL).orElse(RED_FILL));
statusIndicator.strokeProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).filter(Boolean.TRUE::equals).map(unlocked -> GREEN_STROKE).orElse(RED_STROKE));
tooltipProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::path).map(p -> new Tooltip(p.toString())));
tooltipProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::displayablePath).map(p -> new Tooltip(p.toString())));
contextMenuProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).map(unlocked -> {
return unlocked ? null : vaultContextMenu;
}));
@@ -11,7 +11,6 @@ import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.KeyFile;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +18,8 @@ import org.slf4j.LoggerFactory;
public abstract class UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
protected final CryptorProvider cryptorProvider;
protected final Localization localization;
@@ -46,28 +47,28 @@ public abstract class UpgradeStrategy {
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
LOG.info("Upgrading {} from {} to {}.", vault.path().getValue(), vaultVersionBeforeUpgrade, vaultVersionAfterUpgrade);
LOG.info("Upgrading {} from {} to {}.", vault.getPath(), vaultVersionBeforeUpgrade, vaultVersionAfterUpgrade);
Cryptor cryptor = null;
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final Path masterkeyFile = vault.getPath().resolve(MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(masterkeyFileContents), passphrase, vaultVersionBeforeUpgrade);
// create backup, as soon as we know the password was correct:
final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
final Path masterkeyBackupFile = vault.getPath().resolve(MASTERKEY_BACKUP_FILENAME);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
LOG.info("Backuped masterkey.");
// do stuff:
upgrade(vault, cryptor);
// write updated masterkey file:
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase, vaultVersionAfterUpgrade).serialize();
final Path masterkeyFileAfterUpgrade = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
final Path masterkeyFileAfterUpgrade = vault.getPath().resolve(MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrade, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
LOG.info("Updated masterkey.");
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (UnsupportedVaultFormatException e) {
if (e.getDetectedVersion() == Integer.MAX_VALUE) {
LOG.warn("Version MAC authentication error in vault {}", vault.path().get());
LOG.warn("Version MAC authentication error in vault {}", vault.getPath());
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
} else {
LOG.warn("Upgrade failed.", e);
@@ -91,7 +92,7 @@ public abstract class UpgradeStrategy {
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
public boolean isApplicable(Vault vault) {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final Path masterkeyFile = vault.getPath().resolve(MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
@@ -39,7 +39,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
@Override
public String getMessage(Vault vault) {
String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
Path path = vault.path().getValue();
Path path = vault.getPath();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
return String.format(fmt, oldVaultName, newVaultName);
@@ -47,7 +47,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path path = vault.path().getValue();
Path path = vault.getPath();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
Path newPath = path.resolveSibling(newVaultName);
@@ -60,7 +60,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
LOG.info("Renaming {} to {}", path, newPath.getFileName());
Files.move(path, path.resolveSibling(newVaultName));
Platform.runLater(() -> {
vault.setPath(newPath);
vault.getVaultSettings().setPath(newPath);
settings.save();
});
} catch (IOException e) {
@@ -72,7 +72,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
@Override
public boolean isApplicable(Vault vault) {
Path vaultPath = vault.path().getValue();
Path vaultPath = vault.getPath();
if (vaultPath.toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
return super.isApplicable(vault);
} else {
@@ -61,8 +61,8 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.path().get().resolve("d");
Path metadataDir = vault.path().get().resolve("m");
Path dataDir = vault.getPath().resolve("d");
Path metadataDir = vault.getPath().resolve("m");
if (!Files.isDirectory(dataDir)) {
return; // empty vault. no migration needed.
}
@@ -51,7 +51,7 @@ class UpgradeVersion4to5 extends UpgradeStrategy {
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.path().get().resolve("d");
Path dataDir = vault.getPath().resolve("d");
if (!Files.isDirectory(dataDir)) {
return; // empty vault. no migration needed.
}
@@ -76,6 +76,7 @@ class UpgradeVersion4to5 extends UpgradeStrategy {
LOG.info("Migration finished.");
}
@SuppressWarnings("deprecation")
private void migrate(Path file, BasicFileAttributes attrs, Cryptor cryptor) throws IOException {
LOG.info("Starting migration of {}...", file);
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
@@ -8,96 +8,61 @@
*******************************************************************************/
package org.cryptomator.ui.model;
import static org.apache.commons.lang3.StringUtils.stripStart;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryNotEmptyException;
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.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.CharUtils;
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.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.nio.NioFileSystem;
import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory;
import org.cryptomator.filesystem.stats.StatsFileSystem;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.ui.model.VaultModule.PerVault;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.FXThreads;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Vault implements CryptoFileSystemDelegate {
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemDelegate.class);
@PerVault
public class Vault {
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty<Path> path;
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final VaultSettings vaultSettings;
private final WebDavServer server;
private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final BooleanProperty mounted = new SimpleBooleanProperty();
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
private final AtomicReference<FileSystem> nioFileSystem = new AtomicReference<>();
private final String id;
private String mountName;
private Character winDriveLetter;
private Optional<StatsFileSystem> statsFileSystem = Optional.empty();
private DeferredClosable<Frontend> filesystemFrontend = DeferredClosable.empty();
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
/**
* Package private constructor, use {@link VaultFactory}.
*
* @param string
*/
Vault(String id, Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = new SimpleObjectProperty<Path>(vaultDirectoryPath);
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
@Inject
Vault(VaultSettings vaultSettings, WebDavServer server, DeferredCloser closer) {
this.vaultSettings = vaultSettings;
this.server = server;
this.closer = closer;
this.id = id;
try {
setMountName(name().getValue());
} catch (IllegalArgumentException e) {
@@ -105,129 +70,92 @@ public class Vault implements CryptoFileSystemDelegate {
}
}
private FileSystem getNioFileSystem() {
return LazyInitializer.initializeLazily(nioFileSystem, () -> NioFileSystem.rootedAt(path.getValue()));
}
// ******************************************************************************
// Commands
// ********************************************************************************/
private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException {
return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class);
}
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws IOException {
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withPassphrase(passphrase).build();
CryptoFileSystem fs = CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
closer.closeLater(fs);
return fs;
}
public void create(CharSequence passphrase) throws IOException {
try {
FileSystem fs = getNioFileSystem();
if (fs.files().map(File::name).filter(s -> s.endsWith(VAULT_FILE_EXTENSION)).count() > 0) {
throw new FileAlreadyExistsException("masterkey.cryptomator", null, "Vault location not empty.");
} else if (fs.folders().count() > 0) {
throw new DirectoryNotEmptyException(fs.toString());
}
cryptoFileSystemFactory.initializeNew(fs, passphrase);
} catch (UncheckedIOException e) {
throw new IOException(e);
}
getCryptoFileSystem(passphrase);
// TODO and now?
}
public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
try {
cryptoFileSystemFactory.changePassphrase(getNioFileSystem(), oldPassphrase, newPassphrase);
} catch (UncheckedIOException e) {
throw new IOException(e);
}
// TODO implement
}
public synchronized void activateFrontend(FrontendFactory frontendFactory, Settings settings, CharSequence passphrase) throws FrontendCreationFailedException {
public synchronized void activateFrontend(CharSequence passphrase) {
boolean launchSuccess = false;
boolean mountSuccess = false;
try {
FileSystem fs = getNioFileSystem();
FileSystem shorteningFs = shorteningFileSystemFactory.get(fs);
FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(shorteningFs, passphrase, this);
FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC);
StatsFileSystem statsFs = new StatsFileSystem(normalizingFs);
statsFileSystem = Optional.of(statsFs);
Frontend frontend = frontendFactory.create(statsFs, FrontendId.from(id), stripStart(mountName, "/"));
launchSuccess = true;
filesystemFrontend = closer.closeLater(frontend);
frontend.mount(getMountParams(settings));
mountSuccess = true;
} catch (UncheckedIOException e) {
throw new FrontendCreationFailedException(e);
} catch (CommandFailedException e) {
LOG.error("Failed to mount vault " + mountName, e);
FileSystem fs = getCryptoFileSystem(passphrase);
if (!server.isRunning()) {
server.start();
}
server.startWebDavServlet(fs.getPath("/"), vaultSettings.getId());
} catch (IOException e) {
LOG.error("Unable to provide frontend", e);
} finally {
// unlocked is a observable property and should only be changed by the FX application thread
boolean finalLaunchSuccess = launchSuccess;
boolean finalMountSuccess = mountSuccess;
Platform.runLater(() -> {
unlocked.set(finalLaunchSuccess);
mounted.set(finalMountSuccess);
unlocked.set(launchSuccess);
mounted.set(mountSuccess);
});
}
}
public synchronized void deactivateFrontend() throws Exception {
filesystemFrontend.close();
statsFileSystem = Optional.empty();
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
if (fs != null) {
fs.close();
}
// TODO overheadhunter remove servlet from server
Platform.runLater(() -> {
mounted.set(false);
unlocked.set(false);
});
}
private Map<MountParam, Optional<String>> getMountParams(Settings settings) {
String hostname = SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6() ? "0--1.ipv6-literal.net" : "localhost";
return ImmutableMap.of( //
MountParam.MOUNT_NAME, Optional.ofNullable(mountName), //
MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)), //
MountParam.HOSTNAME, Optional.of(hostname), //
MountParam.PREFERRED_GVFS_SCHEME, Optional.ofNullable(settings.getPreferredGvfsScheme()) //
);
}
public synchronized void reveal() throws CommandFailedException {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::reveal);
}
// ******************************************************************************
// Delegate methods
// ********************************************************************************/
@Override
public void authenticationFailed(String cleartextPath) {
namesOfResourcesWithInvalidMac.add(cleartextPath);
}
@Override
public boolean shouldSkipAuthentication(String cleartextPath) {
return whitelistedResourcesWithInvalidMac.contains(cleartextPath);
public synchronized void reveal() {
// TODO implement
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
public VaultSettings getVaultSettings() {
return vaultSettings;
}
public synchronized String getWebDavUrl() {
return filesystemFrontend.get().map(Frontend::getWebDavUrl).orElseThrow(IllegalStateException::new);
// TODO implement
return "http://localhost/not/implemented";
}
void setPath(Path path) {
this.path.set(path);
this.nioFileSystem.set(null);
}
public ReadOnlyObjectProperty<Path> path() {
return path;
public Path getPath() {
return vaultSettings.pathProperty().getValue();
}
public Binding<String> displayablePath() {
Path homeDir = Paths.get(SystemUtils.USER_HOME);
return EasyBind.map(path, p -> {
return EasyBind.map(vaultSettings.pathProperty(), p -> {
if (p.startsWith(homeDir)) {
Path relativePath = homeDir.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return path.getValue().toString();
return p.toString();
}
});
}
@@ -236,16 +164,16 @@ public class Vault implements CryptoFileSystemDelegate {
* @return Directory name without preceeding path components and file extension
*/
public Binding<String> name() {
return EasyBind.map(path, p -> p.getFileName().toString());
return EasyBind.map(vaultSettings.pathProperty(), p -> p.getFileName().toString());
}
public boolean doesVaultDirectoryExist() {
return Files.isDirectory(path.getValue());
return Files.isDirectory(getPath());
}
public boolean isValidVaultDirectory() {
try {
return doesVaultDirectoryExist() && cryptoFileSystemFactory.isValidVaultStructure(getNioFileSystem());
return doesVaultDirectoryExist(); // TODO: && cryptoFileSystemFactory.isValidVaultStructure(getNioFileSystem());
} catch (UncheckedIOException e) {
return false;
}
@@ -268,19 +196,23 @@ public class Vault implements CryptoFileSystemDelegate {
}
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
// TODO overheadhunter implement.
return null;
}
public Set<String> getWhitelistedResourcesWithInvalidMac() {
return whitelistedResourcesWithInvalidMac;
// TODO overheadhunter implement.
return null;
}
public long pollBytesRead() {
return statsFileSystem.map(StatsFileSystem::getThenResetBytesRead).orElse(0l);
// TODO overheadhunter implement.
return 0l;
}
public long pollBytesWritten() {
return statsFileSystem.map(StatsFileSystem::getThenResetBytesWritten).orElse(0l);
// TODO overheadhunter implement.
return 0l;
}
/**
@@ -310,7 +242,7 @@ public class Vault implements CryptoFileSystemDelegate {
}
public String getMountName() {
return mountName;
return vaultSettings.getMountName();
}
/**
@@ -320,23 +252,32 @@ public class Vault implements CryptoFileSystemDelegate {
* @throws IllegalArgumentException if the name is empty after normalization
*/
public void setMountName(String mountName) throws IllegalArgumentException {
mountName = normalize(mountName);
if (StringUtils.isEmpty(mountName)) {
String normalized = normalize(mountName);
if (StringUtils.isEmpty(normalized)) {
throw new IllegalArgumentException("mount name is empty");
} else {
vaultSettings.setMountName(normalized);
}
this.mountName = mountName;
}
public Character getWinDriveLetter() {
return winDriveLetter;
if (vaultSettings.getWinDriveLetter() == null) {
return null;
} else {
return vaultSettings.getWinDriveLetter().charAt(0);
}
}
public void setWinDriveLetter(Character winDriveLetter) {
this.winDriveLetter = winDriveLetter;
if (winDriveLetter == null) {
vaultSettings.setWinDriveLetter(null);
} else {
vaultSettings.setWinDriveLetter(String.valueOf(winDriveLetter));
}
}
public String getId() {
return id;
return vaultSettings.getId();
}
// ******************************************************************************
@@ -345,14 +286,14 @@ public class Vault implements CryptoFileSystemDelegate {
@Override
public int hashCode() {
return path.getValue().hashCode();
return Objects.hash(vaultSettings);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Vault) {
if (obj instanceof Vault && obj.getClass().equals(this.getClass())) {
final Vault other = (Vault) obj;
return this.path.getValue().equals(other.path.getValue());
return Objects.equals(this.vaultSettings, other.vaultSettings);
} else {
return false;
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.model;
import org.cryptomator.ui.model.VaultModule.PerVault;
import dagger.Subcomponent;
@PerVault
@Subcomponent(modules = {VaultModule.class})
public interface VaultComponent {
Vault vault();
}
@@ -8,36 +8,34 @@
*******************************************************************************/
package org.cryptomator.ui.model;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.CryptomatorComponent;
import org.cryptomator.ui.settings.VaultSettings;
@Singleton
public class VaultFactory {
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
private final CryptomatorComponent cryptomatorComponent;
private final ConcurrentMap<VaultSettings, Vault> vaults = new ConcurrentHashMap<>();
@Inject
public VaultFactory(ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
this.closer = closer;
public VaultFactory(CryptomatorComponent cryptomatorComponent) {
this.cryptomatorComponent = cryptomatorComponent;
}
public Vault createVault(String id, Path path) {
return new Vault(id, path, shorteningFileSystemFactory, cryptoFileSystemFactory, closer);
public Vault get(VaultSettings vaultSettings) {
return vaults.computeIfAbsent(vaultSettings, this::create);
}
public Vault createVault(Path path) {
return createVault(FrontendId.generate().toString(), path);
private Vault create(VaultSettings vaultSettings) {
VaultModule module = new VaultModule(vaultSettings);
VaultComponent comp = cryptomatorComponent.newVaultComponent(module);
return comp.vault();
}
}
@@ -0,0 +1,121 @@
package org.cryptomator.ui.model;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.VaultSettings;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.TransformationList;
@Singleton
public class VaultList extends TransformationList<Vault, VaultSettings> {
private final VaultFactory vaultFactory;
private final ObservableList<VaultSettings> source;
@Inject
public VaultList(Settings settings, VaultFactory vaultFactory) {
this(FXCollections.observableList(settings.getDirectories()), settings, vaultFactory);
}
private VaultList(ObservableList<VaultSettings> source, Settings settings, VaultFactory vaultFactory) {
super(source);
this.source = source;
this.vaultFactory = vaultFactory;
addListener((Change<? extends Vault> change) -> settings.save());
}
@Override
public int getSourceIndex(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(Change<? extends VaultSettings> c) {
this.fireChange(new VaultListChange(c));
}
private class VaultListChange extends Change<Vault> {
private final Change<? extends VaultSettings> delegate;
public VaultListChange(Change<? extends VaultSettings> delegate) {
super(VaultList.this);
this.delegate = delegate;
}
@Override
public boolean next() {
return delegate.next();
}
@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() {
List<Vault> removed = new ArrayList<>();
for (VaultSettings s : delegate.getRemoved()) {
removed.add(vaultFactory.get(s));
}
return removed;
}
@Override
protected int[] getPermutation() {
if (delegate.wasPermutated()) {
int len = getTo() - getFrom();
int[] permutations = new int[len];
for (int i = 0; i < len; i++) {
permutations[i] = getPermutation(i);
}
return permutations;
} else {
return new int[0];
}
}
}
}
@@ -0,0 +1,36 @@
package org.cryptomator.ui.model;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import javax.inject.Scope;
import org.cryptomator.ui.settings.VaultSettings;
import dagger.Module;
import dagger.Provides;
@Module
public class VaultModule {
private final VaultSettings vaultSettings;
public VaultModule(VaultSettings vaultSettings) {
this.vaultSettings = Objects.requireNonNull(vaultSettings);
}
@Provides
@PerVault
public VaultSettings provideVaultSettings() {
return vaultSettings;
}
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface PerVault {
}
}
@@ -1,97 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016 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.FileSystems;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.module.SimpleModule;
@Singleton
public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
private final VaultFactory vaultFactoy;
@Inject
public VaultObjectMapperProvider(final VaultFactory vaultFactoy) {
this.vaultFactoy = vaultFactoy;
}
@Override
public ObjectMapper get() {
final ObjectMapper om = new ObjectMapper();
final SimpleModule module = new SimpleModule("VaultJsonMapper");
module.addSerializer(Vault.class, new VaultSerializer());
module.addDeserializer(Vault.class, new VaultDeserializer());
om.registerModule(module);
return om;
}
private static class VaultSerializer extends JsonSerializer<Vault> {
@Override
public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.path().getValue().toString());
jgen.writeStringField("mountName", value.getMountName());
jgen.writeStringField("id", value.getId());
final Character winDriveLetter = value.getWinDriveLetter();
if (winDriveLetter != null) {
jgen.writeStringField("winDriveLetter", Character.toString(winDriveLetter));
}
jgen.writeEndObject();
}
}
private class VaultDeserializer extends JsonDeserializer<Vault> {
@Override
public Vault deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
if (node == null || !node.has("path")) {
throw new InvalidFormatException("Node is null or doesn't contain a path.", node, Vault.class);
}
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
final Vault vault;
if (node.has("id")) {
vault = vaultFactoy.createVault(node.get("id").asText(), path);
} else {
vault = vaultFactoy.createVault(path);
}
if (node.has("mountName")) {
vault.setMountName(node.get("mountName").asText());
}
if (node.has("winDriveLetter")) {
vault.setWinDriveLetter(CharUtils.toCharacterObject(node.get("winDriveLetter").asText()));
}
return vault;
}
}
}
@@ -1,177 +0,0 @@
package org.cryptomator.ui.model;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.ui.settings.Settings;
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
@Singleton
public class Vaults implements ObservableList<Vault> {
private final ObservableList<Vault> delegate;
@Inject
public Vaults(Settings settings) {
this.delegate = FXCollections.observableList(settings.getDirectories());
addListener((Change<? extends Vault> change) -> settings.save());
}
public void addListener(ListChangeListener<? super Vault> listener) {
delegate.addListener(listener);
}
public void removeListener(ListChangeListener<? super Vault> listener) {
delegate.removeListener(listener);
}
public void addListener(InvalidationListener listener) {
delegate.addListener(listener);
}
public boolean addAll(Vault... elements) {
return delegate.addAll(elements);
}
public boolean setAll(Vault... elements) {
return delegate.setAll(elements);
}
public boolean setAll(Collection<? extends Vault> col) {
return delegate.setAll(col);
}
public boolean removeAll(Vault... elements) {
return delegate.removeAll(elements);
}
public void removeListener(InvalidationListener listener) {
delegate.removeListener(listener);
}
public boolean retainAll(Vault... elements) {
return delegate.retainAll(elements);
}
public void remove(int from, int to) {
delegate.remove(from, to);
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public Iterator<Vault> iterator() {
return delegate.iterator();
}
public Object[] toArray() {
return delegate.toArray();
}
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public boolean add(Vault e) {
return delegate.add(e);
}
public boolean remove(Object o) {
return delegate.remove(o);
}
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public boolean addAll(Collection<? extends Vault> c) {
return delegate.addAll(c);
}
public boolean addAll(int index, Collection<? extends Vault> c) {
return delegate.addAll(index, c);
}
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public void clear() {
delegate.clear();
}
public Vault get(int index) {
return delegate.get(index);
}
public Vault set(int index, Vault element) {
return delegate.set(index, element);
}
public void add(int index, Vault element) {
delegate.add(index, element);
}
public Vault remove(int index) {
return delegate.remove(index);
}
public int indexOf(Object o) {
return delegate.indexOf(o);
}
public int lastIndexOf(Object o) {
return delegate.lastIndexOf(o);
}
public ListIterator<Vault> listIterator() {
return delegate.listIterator();
}
public ListIterator<Vault> listIterator(int index) {
return delegate.listIterator(index);
}
public List<Vault> subList(int fromIndex, int toIndex) {
return delegate.subList(fromIndex, toIndex);
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || getClass() != obj.getClass()) return false;
return internalEquals((Vaults)obj);
}
private boolean internalEquals(Vaults other) {
return delegate.equals(other.delegate);
}
public int hashCode() {
return delegate.hashCode();
}
}
@@ -0,0 +1,46 @@
package org.cryptomator.ui.model;
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;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
@Singleton
public final class WindowsDriveLetters {
private static final Set<Character> A_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> (char) i).collect(Collectors.toSet());
}
}
@Inject
public WindowsDriveLetters() {
}
public Set<Character> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
}
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(Collectors.toSet());
}
public Set<Character> getAvailableDriveLetters() {
Set<Character> occupiedDriveLetters = getOccupiedDriveLetters();
Predicate<Character> isOccupiedDriveLetter = occupiedDriveLetters::contains;
return A_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
}
}
@@ -65,8 +65,8 @@ public class Localization extends ResourceBundle {
}
// returns null if no resource for given path
private ResourceBundle loadLocalizationFile(String resourcePath) throws IOException {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
private static ResourceBundle loadLocalizationFile(String resourcePath) throws IOException {
try (InputStream in = Localization.class.getResourceAsStream(resourcePath)) {
if (in != null) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
return new PropertyResourceBundle(reader);
@@ -8,20 +8,15 @@
******************************************************************************/
package org.cryptomator.ui.settings;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.cryptomator.ui.model.Vault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
public class Settings {
@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications", "preferredGvfsScheme"})
public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;
public static final int DEFAULT_PORT = 42427;
@@ -32,25 +27,32 @@ public class Settings implements Serializable {
private final Consumer<Settings> saveCmd;
@JsonProperty("directories")
private List<Vault> directories;
@Expose
@SerializedName("directories")
private List<VaultSettings> directories;
@JsonProperty("checkForUpdatesEnabled")
@Expose
@SerializedName("checkForUpdatesEnabled")
private Boolean checkForUpdatesEnabled;
@JsonProperty("port")
@Expose
@SerializedName("port")
private Integer port;
@JsonProperty("useIpv6")
@Expose
@SerializedName("useIpv6")
private Boolean useIpv6;
@JsonProperty("numTrayNotifications")
@Expose
@SerializedName("numTrayNotifications")
private Integer numTrayNotifications;
@JsonProperty("preferredGvfsScheme")
@Expose
@SerializedName("preferredGvfsScheme")
private String preferredGvfsScheme;
@JsonProperty("debugMode")
@Expose
@SerializedName("debugMode")
private Boolean debugMode;
/**
@@ -61,19 +63,21 @@ public class Settings implements Serializable {
}
public void save() {
saveCmd.accept(this);
if (saveCmd != null) {
saveCmd.accept(this);
}
}
/* Getter/Setter */
public List<Vault> getDirectories() {
public List<VaultSettings> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setDirectories(List<Vault> directories) {
public void setDirectories(List<VaultSettings> directories) {
this.directories = directories;
}
@@ -0,0 +1,21 @@
package org.cryptomator.ui.settings;
import java.lang.reflect.Type;
import java.util.function.Consumer;
import com.google.gson.InstanceCreator;
class SettingsInstanceCreator implements InstanceCreator<Settings> {
private final Consumer<Settings> saveCmd;
public SettingsInstanceCreator(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
@Override
public Settings createInstance(Type type) {
return new Settings(saveCmd);
}
}
@@ -10,7 +10,12 @@ package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -25,16 +30,17 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@Singleton
public class SettingsProvider implements Provider<Settings> {
@@ -54,16 +60,21 @@ public class SettingsProvider implements Provider<Settings> {
}
}
private final ObjectMapper objectMapper;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference<Settings> settings = new AtomicReference<>();
private final SettingsInstanceCreator settingsInstanceCreator = new SettingsInstanceCreator(this::scheduleSave);
private final Gson gson;
@Inject
public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
public SettingsProvider() {
GsonBuilder gsonBuilder = new GsonBuilder() //
.setPrettyPrinting().setLenient().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation();
gsonBuilder.registerTypeAdapter(Settings.class, settingsInstanceCreator);
this.gson = gsonBuilder.create();
}
private Path getSettingsPath() throws IOException {
private Path getSettingsPath() {
final String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH);
}
@@ -78,14 +89,19 @@ public class SettingsProvider implements Provider<Settings> {
@Override
public Settings get() {
final Settings settings = new Settings(this::scheduleSave);
try {
final Path settingsPath = getSettingsPath();
final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ);
objectMapper.readerForUpdating(settings).readValue(in);
return LazyInitializer.initializeLazily(settings, this::load);
}
private Settings load() {
Settings settings;
final Path settingsPath = getSettingsPath();
try (InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); //
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
settings = gson.fromJson(reader, Settings.class);
LOG.info("Settings loaded from " + settingsPath);
} catch (IOException e) {
LOG.info("Failed to load settings, creating new one.");
settings = settingsInstanceCreator.createInstance(Settings.class);
}
return settings;
}
@@ -105,12 +121,14 @@ public class SettingsProvider implements Provider<Settings> {
private void save(Settings settings) {
Objects.requireNonNull(settings);
final Path settingsPath = getSettingsPath();
try {
final Path settingsPath = getSettingsPath();
Files.createDirectories(settingsPath.getParent());
final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
LOG.info("Settings saved to " + settingsPath);
try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
gson.toJson(settings, writer);
LOG.info("Settings saved to " + settingsPath);
}
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}
@@ -0,0 +1,108 @@
package org.cryptomator.ui.settings;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Objects;
import java.util.UUID;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@JsonAdapter(VaultSettingsJsonAdapter.class)
public class VaultSettings {
private final String id;
private final Property<Path> path = new SimpleObjectProperty<>();
private final Property<String> mountName = new SimpleStringProperty();
private final Property<String> winDriveLetter = new SimpleStringProperty();
public VaultSettings(String id) {
this.id = Objects.requireNonNull(id);
}
public static VaultSettings withRandomId() {
return new VaultSettings(generateId());
}
private static String generateId() {
return asBase64String(nineBytesFrom(UUID.randomUUID()));
}
private static String asBase64String(byte[] bytes) {
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
return new String(base64Bytes, StandardCharsets.US_ASCII);
}
private static byte[] nineBytesFrom(UUID uuid) {
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
uuidBuffer.putLong(uuid.getMostSignificantBits());
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
uuidBuffer.flip();
return uuidBuffer.array();
}
/* Getter/Setter */
public String getId() {
return id;
}
public Property<Path> pathProperty() {
return path;
}
public Path getPath() {
return path.getValue();
}
public void setPath(Path path) {
this.path.setValue(path);
}
public Property<String> mountNameProperty() {
return mountName;
}
public String getMountName() {
return mountName.getValue();
}
public void setMountName(String mountName) {
this.mountName.setValue(mountName);
}
public Property<String> winDriveLetterProperty() {
return mountName;
}
public String getWinDriveLetter() {
return winDriveLetter.getValue();
}
public void setWinDriveLetter(String winDriveLetter) {
this.winDriveLetter.setValue(winDriveLetter);
}
/* Hashcode/Equals */
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof VaultSettings && obj.getClass().equals(this.getClass())) {
VaultSettings other = (VaultSettings) obj;
return Objects.equals(this.id, other.id);
} else {
return false;
}
}
}
@@ -0,0 +1,64 @@
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
class VaultSettingsJsonAdapter extends TypeAdapter<VaultSettings> {
private static final Logger LOG = LoggerFactory.getLogger(VaultSettingsJsonAdapter.class);
@Override
public void write(JsonWriter out, VaultSettings value) throws IOException {
out.beginObject();
out.name("id").value(value.getId());
out.name("path").value(value.getPath().toString());
out.name("mountName").value(value.getMountName());
out.name("winDriveLetter").value(value.getWinDriveLetter());
out.endObject();
}
@Override
public VaultSettings read(JsonReader in) throws IOException {
String id = null;
String path = null;
String mountName = null;
String winDriveLetter = null;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "id":
id = in.nextString();
break;
case "path":
path = in.nextString();
break;
case "mountName":
mountName = in.nextString();
break;
case "winDriveLetter":
winDriveLetter = in.nextString();
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
}
}
in.endObject();
VaultSettings settings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id);
settings.setPath(Paths.get(path));
settings.setMountName(mountName);
settings.setWinDriveLetter(winDriveLetter);
return settings;
}
}