Added migration workflow

This commit is contained in:
Sebastian Stenzel
2019-09-06 18:55:47 +02:00
parent 602bccae2a
commit fa7421b1b0
11 changed files with 390 additions and 21 deletions

View File

@@ -25,13 +25,14 @@ public class AddVaultSuccessController implements FxController {
this.vault = vault;
}
public void unlockAndClose(ActionEvent actionEvent) {
close(actionEvent);
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
}
@FXML
public void close(ActionEvent actionEvent) {
public void close() {
window.close();
}

View File

@@ -9,7 +9,9 @@ public enum FxmlFile {
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //

View File

@@ -1,9 +1,11 @@
package org.cryptomator.ui.migration;
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
@@ -11,6 +13,7 @@ import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -23,6 +26,7 @@ import javax.inject.Provider;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@Module
abstract class MigrationModule {
@@ -33,16 +37,26 @@ abstract class MigrationModule {
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
}
@Provides
@MigrationWindow
@MigrationScoped
static Map<KeyCodeCombination, Runnable> provideDefaultAccellerators(@MigrationWindow Set<Map.Entry<KeyCombination, Runnable>> accelerators) {
return Map.ofEntries(accelerators.toArray(Map.Entry[]::new));
}
@Provides
@MigrationWindow
@MigrationScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) {
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @MigrationWindow Lazy<Map<KeyCodeCombination, Runnable>> accelerators) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("unlock.title"));
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.sceneProperty().addListener(observable -> {
stage.getScene().getAccelerators().putAll(accelerators.get());
});
windowIcon.ifPresent(stage.getIcons()::add);
return stage;
}
@@ -50,15 +64,36 @@ abstract class MigrationModule {
@Provides
@FxmlScene(FxmlFile.MIGRATION_START)
@MigrationScoped
static Scene provideMigrationStartScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders, @MigrationWindow Stage window) {
Scene scene = fxmlLoaders.createScene("/fxml/migration_start.fxml"); // TODO rename fxml file
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdW, window::close);
return scene;
static Scene provideMigrationStartScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_start.fxml");
}
@Provides
@FxmlScene(FxmlFile.MIGRATION_RUN)
@MigrationScoped
static Scene provideMigrationRunScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_run.fxml");
}
@Provides
@FxmlScene(FxmlFile.MIGRATION_SUCCESS)
@MigrationScoped
static Scene provideMigrationSuccessScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_success.fxml");
}
// ------------------
@Provides
@IntoSet
@MigrationWindow
static Map.Entry<KeyCombination, Runnable> provideCloseWindowShortcut(@MigrationWindow Stage window) {
if (SystemUtils.IS_OS_WINDOWS) {
return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close);
} else {
return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close);
}
}
// ------------------
@@ -67,5 +102,14 @@ abstract class MigrationModule {
@FxControllerKey(MigrationStartController.class)
abstract FxController bindMigrationStartController(MigrationStartController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationRunController.class)
abstract FxController bindMigrationRunController(MigrationRunController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationSuccessController.class)
abstract FxController bindMigrationSuccessController(MigrationSuccessController controller);
}

View File

@@ -0,0 +1,183 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.WritableValue;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
@MigrationScoped
public class MigrationRunController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MigrationRunController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final Vault vault;
private final ExecutorService executor;
private final Optional<KeychainAccess> keychainAccess;
private final Lazy<Scene> startScene;
private final Lazy<Scene> successScene;
private final ObjectBinding<ContentDisplay> migrateButtonContentDisplay;
private final BooleanProperty migrationButtonDisabled;
public NiceSecurePasswordField passwordField;
@Inject
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.keychainAccess = keychainAccess;
this.startScene = startScene;
this.successScene = successScene;
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
this.migrationButtonDisabled = new SimpleBooleanProperty();
}
public void initialize() {
if (keychainAccess.isPresent()) {
loadStoredPassword();
}
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
}
@FXML
public void back() {
window.setScene(startScene.get());
}
@FXML
public void migrate() {
LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING);
Tasks.create(() -> {
Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) {
vault.setState(VaultState.NEEDS_MIGRATION);
} else {
vault.setState(VaultState.LOCKED);
passwordField.swipe();
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
}
}).onError(InvalidPassphraseException.class, e -> {
shakeWindow();
passwordField.selectAll();
passwordField.requestFocus();
vault.setState(VaultState.NEEDS_MIGRATION);
}).onError(NoApplicableMigratorException.class, e -> {
LOG.error("Can not migrate vault.", e);
vault.setState(VaultState.ERROR);
// TODO show specific error screen
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e);
vault.setState(VaultState.ERROR);
// TODO show generic error screen
}).runOnce(executor);
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
/* Animations */
private void shakeWindow() {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
Timeline timeline = new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
timeline.play();
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public BooleanProperty migrationButtonDisabledProperty() {
return migrationButtonDisabled;
}
public boolean isMigrationButtonDisabled() {
return migrationButtonDisabled.get();
}
public ObjectBinding<ContentDisplay> migrateButtonContentDisplayProperty() {
return migrateButtonContentDisplay;
}
public ContentDisplay getMigrateButtonContentDisplay() {
switch (vault.getState()) {
case PROCESSING:
return ContentDisplay.LEFT;
default:
return ContentDisplay.TEXT_ONLY;
}
}
}

View File

@@ -16,13 +16,13 @@ public class MigrationStartController implements FxController {
private final Stage window;
private final Vault vault;
private final Lazy<Scene> successScene;
private final Lazy<Scene> runMigrationScene;
@Inject
public MigrationStartController(@MigrationWindow Stage window, @MigrationWindow Vault vault, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> successScene) {
public MigrationStartController(@MigrationWindow Stage window, @MigrationWindow Vault vault, @FxmlScene(FxmlFile.MIGRATION_RUN) Lazy<Scene> runMigrationScene) {
this.window = window;
this.vault = vault;
this.successScene = successScene;
this.runMigrationScene = runMigrationScene;
}
public void initialize() {
@@ -35,7 +35,7 @@ public class MigrationStartController implements FxController {
@FXML
public void proceed() {
window.setScene(runMigrationScene.get());
}
/* Getter/Setter */

View File

@@ -0,0 +1,45 @@
package org.cryptomator.ui.migration;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
@MigrationScoped
public class MigrationSuccessController implements FxController {
private final FxApplication fxApplication;
private final Stage window;
private final Vault vault;
@Inject
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
this.fxApplication = fxApplication;
this.window = window;
this.vault = vault;
}
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault);
}
@FXML
public void close() {
window.close();
}
/* Getter/Setters */
public Vault getVault() {
return vault;
}
}

View File

@@ -5,6 +5,7 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
@@ -37,12 +38,13 @@ public class UnlockSuccessController implements FxController {
this.revealButtonDisabled = new SimpleBooleanProperty();
}
@FXML
public void close() {
LOG.trace("UnlockSuccessController.close()");
window.close();
}
@FXML
public void revealAndClose() {
LOG.trace("UnlockSuccessController.revealAndClose()");
revealButtonState.set(ContentDisplay.LEFT);

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.migration.MigrationRunController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<VBox spacing="6">
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayableName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField"/>
</VBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+X">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Button text="%migration.run.startMigrationBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#migrate" contentDisplay="${controller.migrateButtonContentDisplay}"
disable="${controller.migrationButtonDisabled}">
<graphic>
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
</graphic>
</Button>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -28,15 +28,15 @@
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel format="TODO Your vault &quot;%s&quot; needs to be updated to a newer format. Before proceeding, make sure there is no pending synchronization affecting this vault." arg1="${controller.vault.displayableName}" wrapText="true" />
<CheckBox fx:id="confirmSyncDone" text="TODO Yes, my vault is fully synced"/>
<FormattedLabel format="%migration.start.prompt" arg1="${controller.vault.displayableName}" wrapText="true" />
<CheckBox fx:id="confirmSyncDone" text="%migration.start.confirm"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<ButtonBar buttonMinWidth="120" buttonOrder="C+X">
<buttons>
<Button text="%generic.button.done" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed" disable="${!confirmSyncDone.selected}"/>
</buttons>
</ButtonBar>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.migration.MigrationSuccessController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%migration.success.nextStepsInstructions" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.done" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%migration.success.unlockNow" ButtonBar.buttonData="FINISH" onAction="#unlockAndClose" defaultButton="true"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -81,6 +81,18 @@ unlock.deleteSavedPasswordDialog.title=Delete Saved Password
unlock.deleteSavedPasswordDialog.header=Do you really want to delete the saved password of this vault?
unlock.deleteSavedPasswordDialog.content=The saved password of this vault will be immediately deleted from your system keychain. If you'd like to save your password again, you'd have to unlock your vault with the "Save Password" option enabled.
# Migration
migration.title=Upgrade Vault
## Start
migration.start.prompt=Your vault "%s" needs to be updated to a newer format. Before proceeding, make sure there is no pending synchronization affecting this vault.
migration.start.confirm=Yes, my vault is fully synced
## Run
migration.run.enterPassword=Enter the password for "%s"
migration.run.startMigrationBtn=Migrate Vault
## Sucess
migration.success.nextStepsInstructions=Migrated "%s" successfully.\nYou can now unlock your vault.
migration.success.unlockNow=Unlock Now
# Preferences
preferences.title=Preferences
## General