diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 732b66fef..0aae9a541 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -10,6 +10,7 @@ package org.cryptomator.common.settings; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; import org.cryptomator.common.Environment; import org.cryptomator.common.LazyInitializer; import org.slf4j.Logger; @@ -29,6 +30,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.Optional; import java.util.concurrent.Executors; @@ -109,12 +111,14 @@ public class SettingsProvider implements Provider { LOG.debug("Attempting to save settings to {}", settingsPath); try { Files.createDirectories(settingsPath.getParent()); - try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // - Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp"); + try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW); // + Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { gson.toJson(settings, writer); - LOG.info("Settings saved to {}", settingsPath); } - } catch (IOException e) { + Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING); + LOG.info("Settings saved to {}", settingsPath); + } catch (IOException | JsonParseException e) { LOG.error("Failed to save settings.", e); } } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 41c91efcf..0a0301266 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Base64; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -34,9 +35,10 @@ public class VaultSettings { public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true; public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false; public static final boolean DEFAULT_USES_READONLY_MODE = false; + public static final String DEFAULT_MOUNT_FLAGS = ""; private final String id; - private final ObjectProperty path = new SimpleObjectProperty<>(); + private final ObjectProperty path = new SimpleObjectProperty(); private final StringProperty mountName = new SimpleStringProperty(); private final StringProperty winDriveLetter = new SimpleStringProperty(); private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP); @@ -44,6 +46,7 @@ public class VaultSettings { private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH); private final StringProperty individualMountPath = new SimpleStringProperty(); private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE); + private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); public VaultSettings(String id) { this.id = Objects.requireNonNull(id); @@ -52,7 +55,7 @@ public class VaultSettings { } Observable[] observables() { - return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode}; + return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags}; } private void deriveMountNameFromPath(Path path) { @@ -147,6 +150,10 @@ public class VaultSettings { return usesReadOnlyMode; } + public StringProperty mountFlags() { + return mountFlags; + } + /* Hashcode/Equals */ @Override diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 7a3f03b39..e5130cdb0 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -26,8 +26,9 @@ class VaultSettingsJsonAdapter { out.name("unlockAfterStartup").value(value.unlockAfterStartup().get()); out.name("revealAfterMount").value(value.revealAfterMount().get()); out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get()); - out.name("individualMountPath").value(value.individualMountPath().get()); //TODO: should this always be written? ( because it could contain metadata, which the user may not want to save!) + out.name("individualMountPath").value(value.individualMountPath().get()); out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get()); + out.name("mountFlags").value(value.mountFlags().get()); out.endObject(); } @@ -41,6 +42,7 @@ class VaultSettingsJsonAdapter { boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT; boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH; boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; + String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS; in.beginObject(); while (in.hasNext()) { @@ -73,6 +75,9 @@ class VaultSettingsJsonAdapter { case "usesReadOnlyMode": usesReadOnlyMode = in.nextBoolean(); break; + case "mountFlags": + mountFlags = in.nextString(); + break; default: LOG.warn("Unsupported vault setting found in JSON: " + name); in.skipValue(); @@ -90,6 +95,7 @@ class VaultSettingsJsonAdapter { vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath); vaultSettings.individualMountPath().set(individualMountPath); vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode); + vaultSettings.mountFlags().set(mountFlags); return vaultSettings; } diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java index 32f393842..62a37a5c2 100644 --- a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java @@ -6,12 +6,17 @@ package org.cryptomator.common.settings; import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringReader; +import java.io.StringWriter; import java.nio.file.Paths; +import java.util.Arrays; public class VaultSettingsJsonAdapterTest { @@ -19,7 +24,7 @@ public class VaultSettingsJsonAdapterTest { @Test public void testDeserialize() throws IOException { - String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\"}"; + String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\", \"mountFlags\":\"--foo --bar\"}"; JsonReader jsonReader = new JsonReader(new StringReader(json)); VaultSettings vaultSettings = adapter.read(jsonReader); @@ -28,6 +33,27 @@ public class VaultSettingsJsonAdapterTest { Assertions.assertEquals("test", vaultSettings.mountName().get()); Assertions.assertEquals("X", vaultSettings.winDriveLetter().get()); Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get()); + Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get()); + + + } + + @Test + public void testSerialize() throws IOException { + VaultSettings vaultSettings = new VaultSettings("test"); + vaultSettings.path().set(Paths.get("/foo/bar")); + vaultSettings.mountName().set("mountyMcMountFace"); + vaultSettings.mountFlags().set("--foo --bar"); + + StringWriter buf = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(buf); + adapter.write(jsonWriter, vaultSettings); + String result = buf.toString(); + + MatcherAssert.assertThat(result, CoreMatchers.containsString("\"id\":\"test\"")); + MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"/foo/bar\"")); + MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountName\":\"mountyMcMountFace\"")); + MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountFlags\":\"--foo --bar\"")); } } diff --git a/main/pom.xml b/main/pom.xml index ffe4cb6c5..ad93007b5 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -27,8 +27,8 @@ 1.2.1 1.8.5 2.0.0 - 1.1.3 - 1.1.8 + 1.2.0 + 1.1.9 1.0.10 12 diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 3d90c689f..ac2d1fb37 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -11,6 +11,8 @@ package org.cryptomator.ui.controllers; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import javafx.application.Application; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; @@ -20,13 +22,13 @@ import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; -import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; @@ -83,6 +85,7 @@ public class UnlockController implements ViewController { private Vault vault; private Optional listener = Optional.empty(); private Subscription vaultSubs = Subscription.EMPTY; + private BooleanProperty unlocking = new SimpleBooleanProperty(); @Inject public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional keychainAccess, Settings settings, ExecutorService executor) { @@ -113,11 +116,17 @@ public class UnlockController implements ViewController { @FXML private TextField mountName; + @FXML + private CheckBox useCustomMountFlags; + + @FXML + private TextField mountFlags; + @FXML private CheckBox revealAfterMount; @FXML - private Label winDriveLetterLabel; + private CheckBox useCustomWinDriveLetter; @FXML private ChoiceBox winDriveLetter; @@ -131,20 +140,14 @@ public class UnlockController implements ViewController { @FXML private Label customMountPointLabel; - @FXML - private ProgressIndicator progressIndicator; - - @FXML - private Text progressText; - @FXML private Hyperlink downloadsPageLink; @FXML - private GridPane advancedOptions; + private VBox advancedOptions; @FXML - private GridPane root; + private VBox root; @FXML private CheckBox unlockAfterStartup; @@ -155,25 +158,31 @@ public class UnlockController implements ViewController { @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); - unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); + advancedOptions.disableProperty().bind(unlocking); + unlockButton.disableProperty().bind(unlocking.or(passwordField.textProperty().isEmpty())); mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); + useReadOnlyMode.selectedProperty().addListener(this::useReadOnlyDidChange); + useCustomMountFlags.selectedProperty().addListener(this::useCustomMountFlagsDidChange); + mountFlags.disableProperty().bind(useCustomMountFlags.selectedProperty().not()); + mountFlags.textProperty().addListener(this::mountFlagsDidChange); savePassword.setDisable(!keychainAccess.isPresent()); unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not())); + downloadsPageLink.visibleProperty().bind(downloadsPageLink.managedProperty()); customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty()); customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty()); winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); + winDriveLetter.disableProperty().bind(useCustomWinDriveLetter.selectedProperty().not()); if (!SystemUtils.IS_OS_WINDOWS) { - winDriveLetterLabel.setVisible(false); - winDriveLetterLabel.setManaged(false); + useCustomWinDriveLetter.setVisible(false); + useCustomWinDriveLetter.setManaged(false); winDriveLetter.setVisible(false); winDriveLetter.setManaged(false); } } - @Override public Parent getRoot() { return root; @@ -198,20 +207,12 @@ public class UnlockController implements ViewController { this.vault = vault; advancedOptions.setVisible(false); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); - progressIndicator.setVisible(false); - progressText.setText(null); + unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY); state.successMessage().map(localization::getString).ifPresent(messageText::setText); - if (SystemUtils.IS_OS_WINDOWS) { - winDriveLetter.valueProperty().removeListener(driveLetterChangeListener); - winDriveLetter.getItems().clear(); - winDriveLetter.getItems().add(null); - winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters()); - winDriveLetter.getItems().sort(new WinDriveLetterComparator()); - winDriveLetter.valueProperty().addListener(driveLetterChangeListener); - chooseSelectedDriveLetter(); - } - downloadsPageLink.setVisible(false); + downloadsPageLink.setManaged(false); mountName.setText(vault.getMountName()); + useCustomMountFlags.setSelected(vault.isHavingCustomMountFlags()); + mountFlags.setText(vault.getMountFlags()); savePassword.setSelected(false); // auto-fill pw from keychain: if (keychainAccess.isPresent()) { @@ -226,6 +227,7 @@ public class UnlockController implements ViewController { VaultSettings vaultSettings = vault.getVaultSettings(); unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get()); revealAfterMount.setSelected(vaultSettings.revealAfterMount().get()); + useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get()); // WEBDAV-dependent controls: if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) { @@ -241,26 +243,21 @@ public class UnlockController implements ViewController { } } - // DOKANY-dependent controls: - if (VolumeImpl.DOKANY.equals(settings.preferredVolumeImpl().get())) { - winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); - winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); - winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); - winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); - // readonly not yet supported by dokany - useReadOnlyMode.setSelected(false); - useReadOnlyMode.setVisible(false); - useReadOnlyMode.setManaged(false); - } else { - useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get()); - } // OS-dependent controls: if (SystemUtils.IS_OS_WINDOWS) { + winDriveLetter.valueProperty().removeListener(driveLetterChangeListener); + winDriveLetter.getItems().clear(); + winDriveLetter.getItems().add(null); + winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters()); + winDriveLetter.getItems().sort(new WinDriveLetterComparator()); + winDriveLetter.valueProperty().addListener(driveLetterChangeListener); + chooseSelectedDriveLetter(); + winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); - winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); - winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); + useCustomWinDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); + useCustomWinDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); } vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set)); @@ -297,6 +294,7 @@ public class UnlockController implements ViewController { @FXML private void didClickAdvancedOptionsButton() { messageText.setText(null); + downloadsPageLink.setManaged(false); advancedOptions.setVisible(!advancedOptions.isVisible()); if (advancedOptions.isVisible()) { advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide")); @@ -318,6 +316,30 @@ public class UnlockController implements ViewController { } else { vault.setMountName(newValue); } + if (!useCustomMountFlags.isSelected()) { + mountFlags.setText(vault.getMountFlags()); // update default flags + } + } + + private void useReadOnlyDidChange(@SuppressWarnings("unused") ObservableValue property, @SuppressWarnings("unused")Boolean oldValue, Boolean newValue) { + vault.getVaultSettings().usesReadOnlyMode().setValue(newValue); + if (!useCustomMountFlags.isSelected()) { + mountFlags.setText(vault.getMountFlags()); // update default flags + } + } + + + private void useCustomMountFlagsDidChange(@SuppressWarnings("unused") ObservableValue property, @SuppressWarnings("unused")Boolean oldValue, Boolean newValue) { + if (!newValue) { + vault.setMountFlags(VaultSettings.DEFAULT_MOUNT_FLAGS); + mountFlags.setText(vault.getMountFlags()); + } + } + + private void mountFlagsDidChange(@SuppressWarnings("unused") ObservableValue property, @SuppressWarnings("unused")String oldValue, String newValue) { + if (useCustomMountFlags.isSelected()) { + vault.setMountFlags(newValue); + } } @FXML @@ -330,6 +352,13 @@ public class UnlockController implements ViewController { } } + @FXML + public void didClickCustomWinDriveLetterCheckbox() { + if (!useCustomWinDriveLetter.isSelected()) { + winDriveLetter.setValue(null); + } + } + /** * Converts 'C' to "C:" to translate between model and GUI. */ @@ -379,7 +408,7 @@ public class UnlockController implements ViewController { private void chooseSelectedDriveLetter() { assert SystemUtils.IS_OS_WINDOWS; // if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this: - if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { + if (vault.getWinDriveLetter() != null && driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { vault.setWinDriveLetter(null); } final Character letter = vault.getWinDriveLetter(); @@ -428,35 +457,34 @@ public class UnlockController implements ViewController { @FXML private void didClickUnlockButton() { - advancedOptions.setDisable(true); - advancedOptions.setVisible(false); + unlocking.set(true); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); - progressIndicator.setVisible(true); + unlockButton.setContentDisplay(ContentDisplay.LEFT); CharSequence password = passwordField.getCharacters(); Tasks.create(() -> { - progressText.setText(localization.getString("unlock.pendingMessage.unlocking")); vault.unlock(password); if (keychainAccess.isPresent() && savePassword.isSelected()) { keychainAccess.get().storePassphrase(vault.getId(), password); } }).onSuccess(() -> { - messageText.setText(""); - downloadsPageLink.setVisible(false); + messageText.setText(null); + downloadsPageLink.setManaged(false); listener.ifPresent(lstnr -> lstnr.didUnlock(vault)); passwordField.swipe(); }).onError(InvalidPassphraseException.class, e -> { messageText.setText(localization.getString("unlock.errorMessage.wrongPassword")); + downloadsPageLink.setManaged(false); passwordField.selectAll(); passwordField.requestFocus(); }).onError(UnsupportedVaultFormatException.class, e -> { if (e.isVaultOlderThanSoftware()) { // whitespace after localized text used as separator between text and hyperlink messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); - downloadsPageLink.setVisible(true); + downloadsPageLink.setManaged(true); } else if (e.isSoftwareOlderThanVault()) { messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); - downloadsPageLink.setVisible(true); + downloadsPageLink.setManaged(true); } else if (e.getDetectedVersion() == Integer.MAX_VALUE) { messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac")); } @@ -464,19 +492,21 @@ public class UnlockController implements ViewController { LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage()); advancedOptions.setVisible(true); messageText.setText(null); + downloadsPageLink.setManaged(false); showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting"); }).onError(DirectoryNotEmptyException.class, e -> { LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage()); advancedOptions.setVisible(true); messageText.setText(null); + downloadsPageLink.setManaged(false); showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty"); }).onError(Exception.class, e -> { // including RuntimeExceptions LOG.error("Unlock failed for technical reasons.", e); messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); + downloadsPageLink.setManaged(false); }).andFinally(() -> { - advancedOptions.setDisable(false); - progressIndicator.setVisible(false); - progressText.setText(null); + unlocking.set(false); + unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY); }).runOnce(executor); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DefaultMountFlags.java b/main/ui/src/main/java/org/cryptomator/ui/model/DefaultMountFlags.java new file mode 100644 index 000000000..50943a34f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/DefaultMountFlags.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.model; + +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 { +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java b/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java index b9200a981..84d1a300b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java @@ -44,11 +44,11 @@ public class DokanyVolume implements Volume { } @Override - public void mount(CryptoFileSystem fs) throws VolumeException, IOException { + 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); + this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags); } 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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java b/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java index fcd69493d..b740dfcbd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.model; +import com.google.common.base.Splitter; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.VaultSettings; @@ -9,6 +10,7 @@ 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; @@ -42,7 +44,7 @@ public class FuseVolume implements Volume { } @Override - public void mount(CryptoFileSystem fs) throws IOException, FuseNotSupportedException, VolumeException { + public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException { Optional optionalCustomMountPoint = vaultSettings.getIndividualMountPath(); if (optionalCustomMountPoint.isPresent()) { Path customMountPoint = Paths.get(optionalCustomMountPoint.get()); @@ -53,7 +55,7 @@ public class FuseVolume implements Volume { this.mountPoint = prepareTemporaryMountPoint(); LOG.debug("Successfully created mount point: {}", mountPoint); } - mount(fs.getPath("/")); + mount(fs.getPath("/"), mountFlags); } private void checkProvidedMountPoint(Path mountPoint) throws IOException { @@ -94,18 +96,23 @@ public class FuseVolume implements Volume { throw new VolumeException("Did not find feasible mount point."); } - private void mount(Path root) throws VolumeException { + private void mount(Path root, String mountFlags) throws VolumeException { try { + Mounter mounter = FuseMountFactory.getMounter(); EnvironmentVariables envVars = EnvironmentVariables.create() // - .withMountName(vaultSettings.mountName().getValue()) // - .withMountPath(mountPoint) // + .withFlags(splitFlags(mountFlags)) + .withMountPoint(mountPoint) // .build(); - this.fuseMnt = FuseMountFactory.getMounter().mount(root, envVars); + 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 { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/PerVault.java b/main/ui/src/main/java/org/cryptomator/ui/model/PerVault.java new file mode 100644 index 000000000..3172beec0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/PerVault.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.model; + +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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index bd48150fc..325f6ba97 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -25,7 +25,6 @@ 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.cryptomator.ui.model.VaultModule.PerVault; import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +43,7 @@ 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 { @@ -54,6 +54,7 @@ public class Vault { private final VaultSettings vaultSettings; private final Provider volumeProvider; + private final Supplier defaultMountFlags; private final AtomicReference cryptoFileSystem = new AtomicReference<>(); private final ObjectProperty state = new SimpleObjectProperty(State.LOCKED); @@ -64,9 +65,10 @@ public class Vault { } @Inject - Vault(VaultSettings vaultSettings, Provider volumeProvider) { + Vault(VaultSettings vaultSettings, Provider volumeProvider, @DefaultMountFlags Supplier defaultMountFlags) { this.vaultSettings = vaultSettings; this.volumeProvider = volumeProvider; + this.defaultMountFlags = defaultMountFlags; } // ****************************************************************************** @@ -110,7 +112,7 @@ public class Vault { } CryptoFileSystem fs = getCryptoFileSystem(passphrase); volume = volumeProvider.get(); - volume.mount(fs); + volume.mount(fs, getMountFlags()); Platform.runLater(() -> { state.set(State.UNLOCKED); }); @@ -241,10 +243,6 @@ public class Vault { } } - public String getMountName() { - return vaultSettings.mountName().get(); - } - public String getCustomMountPath() { return vaultSettings.individualMountPath().getValueSafe(); } @@ -253,6 +251,10 @@ public class Vault { 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"); @@ -261,6 +263,23 @@ public class Vault { } } + 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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java index 6fafed661..bca3dab63 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java @@ -7,7 +7,6 @@ package org.cryptomator.ui.model; import dagger.BindsInstance; import org.cryptomator.common.settings.VaultSettings; -import org.cryptomator.ui.model.VaultModule.PerVault; import dagger.Subcomponent; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java index 100137f6f..cb416557b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java @@ -7,28 +7,24 @@ package org.cryptomator.ui.model; 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 javax.inject.Scope; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +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); - @Scope - @Documented - @Retention(RetentionPolicy.RUNTIME) - @interface PerVault { - - } - @Provides public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) { VolumeImpl preferredImpl = settings.preferredVolumeImpl().get(); @@ -45,4 +41,91 @@ public class VaultModule { } } + @Provides + @PerVault + @DefaultMountFlags + public Supplier provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) { + 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(); + } + + // 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(); + } + + // 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(" --threadCount 5"); + flags.append(" --timeout 10000"); + flags.append(" --allocation-unit-size 4096"); + flags.append(" --sector-size 4096"); + return flags.toString(); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Volume.java b/main/ui/src/main/java/org/cryptomator/ui/model/Volume.java index 73f8d666b..dc1aae0cd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Volume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Volume.java @@ -22,7 +22,7 @@ public interface Volume { * @param fs * @throws IOException */ - void mount(CryptoFileSystem fs) throws IOException, VolumeException; + void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException; void reveal() throws VolumeException; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/WebDavVolume.java b/main/ui/src/main/java/org/cryptomator/ui/model/WebDavVolume.java index 2161e388d..118e3a368 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/WebDavVolume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/WebDavVolume.java @@ -11,7 +11,6 @@ import org.cryptomator.frontend.webdav.servlet.WebDavServletController; import javax.inject.Inject; import javax.inject.Provider; - import java.net.InetAddress; import java.net.UnknownHostException; @@ -35,7 +34,7 @@ public class WebDavVolume implements Volume { } @Override - public void mount(CryptoFileSystem fs) throws VolumeException { + public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException { if (server == null) { server = serverProvider.get(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java b/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java index 05efef31d..c42696c01 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java @@ -8,6 +8,8 @@ package org.cryptomator.ui.model; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.FxApplicationScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.nio.file.FileSystems; @@ -21,6 +23,7 @@ import java.util.stream.StreamSupport; @FxApplicationScoped public final class WindowsDriveLetters { + private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class); private static final Set D_TO_Z; static { @@ -35,10 +38,12 @@ public final class WindowsDriveLetters { public Set getOccupiedDriveLetters() { if (!SystemUtils.IS_OS_WINDOWS) { - throw new UnsupportedOperationException("This method is only defined for Windows file systems"); + LOG.warn("Attempted to get occupied drive letters on non-Windows machine."); + return Set.of(); + } else { + Iterable rootDirs = FileSystems.getDefault().getRootDirectories(); + return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(Collectors.toSet()); } - Iterable rootDirs = FileSystems.getDefault().getRootDirectories(); - return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(Collectors.toSet()); } public Set getAvailableDriveLetters() { diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 585b60b50..0dfc523e9 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -17,102 +17,85 @@ - + + - + + - + - - - - + + + - - -