mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-22 07:54:19 +00:00
Remove VaultTemplate stuff
including jdk.zipfs as dependency in jlink Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
@@ -126,7 +126,7 @@ jobs:
|
||||
--verbose
|
||||
--output runtime
|
||||
--module-path "${JMOD_PATHS}"
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,jdk.zipfs,java.compiler
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,java.compiler
|
||||
--strip-native-commands
|
||||
--no-header-files
|
||||
--no-man-pages
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
--verbose
|
||||
--output runtime
|
||||
--module-path "${{ steps.jep-493-check.outputs.jmod_paths }}"
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.accessibility,jdk.management.jfr,jdk.zipfs,java.compiler
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.accessibility,jdk.management.jfr,java.compiler
|
||||
--strip-native-commands
|
||||
--no-header-files
|
||||
--no-man-pages
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
--verbose
|
||||
--output runtime
|
||||
--module-path "${{ steps.jep-493-check.outputs.jmod_paths }}"
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.accessibility,jdk.management.jfr,jdk.zipfs,java.compiler
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.accessibility,jdk.management.jfr,java.compiler
|
||||
--strip-native-commands
|
||||
--no-header-files
|
||||
--no-man-pages
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
--verbose
|
||||
--output runtime
|
||||
--module-path "${{ steps.jep-493-check.outputs.jmod_paths }}"
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.crypto.mscapi,jdk.unsupported,jdk.accessibility,jdk.management.jfr,jdk.zipfs,java.compiler
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.crypto.mscapi,jdk.unsupported,jdk.accessibility,jdk.management.jfr,java.compiler
|
||||
--strip-native-commands
|
||||
--no-header-files
|
||||
--no-man-pages
|
||||
|
||||
Vendored
+1
-1
@@ -62,7 +62,7 @@ ${JAVA_HOME}/bin/jlink \
|
||||
--verbose \
|
||||
--output runtime \
|
||||
--module-path "${JMOD_PATHS}" \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,jdk.zipfs,java.compiler \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,java.compiler \
|
||||
--strip-native-commands \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
|
||||
Vendored
+1
-1
@@ -28,7 +28,7 @@ override_dh_auto_build:
|
||||
$(JAVA_HOME)/bin/jlink \
|
||||
--output runtime \
|
||||
--module-path "${JMODS_PATH}" \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,jdk.zipfs,java.compiler \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,java.compiler \
|
||||
--strip-native-commands \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
|
||||
@@ -100,7 +100,7 @@ modules:
|
||||
- $JAVA_HOME/bin/jlink
|
||||
--output runtime
|
||||
--module-path $JMODS_PATH
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,jdk.zipfs,java.compiler
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,java.compiler
|
||||
--no-header-files
|
||||
--no-man-pages
|
||||
--strip-debug
|
||||
|
||||
Vendored
+1
-1
@@ -57,7 +57,7 @@ build() {
|
||||
"$JAVA_HOME/bin/jlink" \
|
||||
--output runtime \
|
||||
--module-path "$JMODS_PATH" \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,jdk.zipfs,java.compiler \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net,java.compiler \
|
||||
--strip-native-commands \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
|
||||
Vendored
+1
-1
@@ -85,7 +85,7 @@ fi
|
||||
${JAVA_HOME}/bin/jlink \
|
||||
--output runtime \
|
||||
--module-path "${JMOD_PATHS}" \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.zipfs,java.compiler \
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,java.compiler \
|
||||
--strip-native-commands \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
|
||||
Vendored
+1
-1
@@ -148,7 +148,7 @@ Invoke-CommandWithExitCheck -Command `
|
||||
"--verbose",
|
||||
"--output", "runtime",
|
||||
"--module-path", $jmodPaths,
|
||||
"--add-modules", "java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.accessibility,jdk.management.jfr,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.crypto.mscapi,jdk.zipfs,java.compiler,javafx.base,javafx.graphics,javafx.controls,javafx.fxml",
|
||||
"--add-modules", "java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.accessibility,jdk.management.jfr,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.crypto.mscapi,java.compiler,javafx.base,javafx.graphics,javafx.controls,javafx.fxml",
|
||||
"--strip-native-commands",
|
||||
"--no-header-files",
|
||||
"--no-man-pages",
|
||||
|
||||
@@ -13,8 +13,6 @@ public enum FxmlFile {
|
||||
CONVERTVAULT_HUBTOPASSWORD_CONVERT("/fxml/convertvault_hubtopassword_convert.fxml"), //
|
||||
CONVERTVAULT_HUBTOPASSWORD_SUCCESS("/fxml/convertvault_hubtopassword_success.fxml"), //
|
||||
DECRYPTNAMES("/fxml/decryptnames.fxml"), //
|
||||
IMPORT_TEMPLATE_LOCATION("/fxml/import_template_location.fxml"), //
|
||||
IMPORT_TEMPLATE_SUCCESS("/fxml/import_template_success.fxml"), //
|
||||
ERROR("/fxml/error.fxml"), //
|
||||
EVENT_VIEW("/fxml/eventview.fxml"), //
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
|
||||
@@ -158,17 +158,6 @@ public class Dialogs {
|
||||
.setOkAction(Stage::close);
|
||||
}
|
||||
|
||||
public SimpleDialog.Builder prepareMalformedTemplateDialog(Stage window) {
|
||||
return createDialogBuilder() //
|
||||
.setOwner(window) //
|
||||
.setTitleKey("importTemplate.title") //
|
||||
.setMessageKey("importTemplate.malformedTemplate.message") //
|
||||
.setDescriptionKey("importTemplate.malformedTemplate.description") //
|
||||
.setIcon(FontAwesome5Icon.EXCLAMATION) //
|
||||
.setOkButtonKey(BUTTON_KEY_CLOSE) //
|
||||
.setOkAction(Stage::close);
|
||||
}
|
||||
|
||||
public SimpleDialog.Builder prepareNoDDirectorySelectedDialog(Stage window) {
|
||||
return createDialogBuilder() //
|
||||
.setOwner(window) //
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.cryptomator.ui.decryptname.DecryptNameComponent;
|
||||
import org.cryptomator.ui.error.ErrorComponent;
|
||||
import org.cryptomator.ui.eventview.EventViewComponent;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
import org.cryptomator.ui.importtemplate.ImportTemplateComponent;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.notification.NotificationComponent;
|
||||
@@ -32,7 +31,6 @@ import java.util.Optional;
|
||||
|
||||
@Module(subcomponents = {TrayMenuComponent.class, //
|
||||
DecryptNameComponent.class, //
|
||||
ImportTemplateComponent.class, //
|
||||
MainWindowComponent.class, //
|
||||
PreferencesComponent.class, //
|
||||
VaultOptionsComponent.class, //
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.cryptomator.ui.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.dialogs.SimpleDialog;
|
||||
import org.cryptomator.ui.error.ErrorComponent;
|
||||
import org.cryptomator.ui.eventview.EventViewComponent;
|
||||
import org.cryptomator.ui.importtemplate.ImportTemplateWindows;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.notification.NotificationComponent;
|
||||
@@ -61,7 +60,6 @@ public class FxApplicationWindows {
|
||||
private final ExecutorService executor;
|
||||
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
||||
private final ShareVaultComponent.Factory shareVaultWindow;
|
||||
private final ImportTemplateWindows importTemplateWindows;
|
||||
private final FilteredList<Window> visibleWindows;
|
||||
private final Dialogs dialogs;
|
||||
|
||||
@@ -77,7 +75,6 @@ public class FxApplicationWindows {
|
||||
ErrorComponent.Factory errorWindowFactory, //
|
||||
VaultOptionsComponent.Factory vaultOptionsWindow, //
|
||||
ShareVaultComponent.Factory shareVaultWindow, //
|
||||
ImportTemplateWindows importTemplateWindows, //
|
||||
EventViewComponent.Factory eventViewWindowFactory, //
|
||||
NotificationComponent.Factory notificationWindowFactory, //
|
||||
ExecutorService executor, //
|
||||
@@ -96,7 +93,6 @@ public class FxApplicationWindows {
|
||||
this.executor = executor;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
this.shareVaultWindow = shareVaultWindow;
|
||||
this.importTemplateWindows = importTemplateWindows;
|
||||
this.visibleWindows = Window.getWindows().filtered(Window::isShowing);
|
||||
this.dialogs = dialogs;
|
||||
}
|
||||
@@ -148,20 +144,6 @@ public class FxApplicationWindows {
|
||||
CompletableFuture.runAsync(() -> shareVaultWindow.create(vault).showShareVaultWindow(), Platform::runLater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the vault template import flow, which decides for itself whether the template can be imported at all.
|
||||
* Unexpected failures (as opposed to an unusable template) surface in the generic error window.
|
||||
*/
|
||||
public CompletionStage<Stage> showImportTemplateWindow(String name, byte[] template) {
|
||||
return showMainWindow() //
|
||||
.thenComposeAsync(_ -> importTemplateWindows.extractAndShowImportTemplateWindow(name, template), Platform::runLater) //
|
||||
.exceptionallyAsync(e -> {
|
||||
showErrorWindow(e, primaryStage, null);
|
||||
return primaryStage;
|
||||
}, Platform::runLater) //
|
||||
.whenComplete(this::reportErrors);
|
||||
}
|
||||
|
||||
public CompletionStage<Stage> showVaultOptionsWindow(Vault vault, SelectedVaultOptionsTab tab) {
|
||||
return showMainWindow().thenApplyAsync(_ -> vaultOptionsWindow.create(vault).showVaultOptionsWindow(tab), Platform::runLater) //
|
||||
.whenComplete(this::reportErrors);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@ImportTemplateScoped
|
||||
@Subcomponent(modules = {ImportTemplateModule.class})
|
||||
public interface ImportTemplateComponent {
|
||||
|
||||
@ImportTemplateWindow
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.IMPORT_TEMPLATE_LOCATION)
|
||||
Lazy<Scene> scene();
|
||||
|
||||
default void showImportTemplateWindow() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
ImportTemplateComponent create(@BindsInstance @Named("vaultName") String name, @BindsInstance VaultTemplate template);
|
||||
}
|
||||
|
||||
}
|
||||
-340
@@ -1,340 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.ObservableUtil;
|
||||
import org.cryptomator.common.locationpresets.LocationPreset;
|
||||
import org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
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.FontAwesome5IconView;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@ImportTemplateScoped
|
||||
public class ImportTemplateLocationController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ImportTemplateLocationController.class);
|
||||
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Path.of(System.getProperty("user.home"));
|
||||
private static final String TEMP_FILE_PREFIX = ".locationTest.cryptomator";
|
||||
|
||||
private final Stage window;
|
||||
private final String vaultName;
|
||||
private final VaultTemplate template;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final ObjectProperty<Vault> vault;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final VaultListManager vaultListManager;
|
||||
private final ExecutorService executor;
|
||||
private final Settings settings;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final ObservableValue<VaultPathStatus> vaultPathStatus;
|
||||
private final ObservableValue<Boolean> validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final BooleanProperty loadingPresetLocations = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty processing = new SimpleBooleanProperty(false);
|
||||
private final ObservableValue<Boolean> readyToImport;
|
||||
private final ObjectBinding<ContentDisplay> createButtonState;
|
||||
private final ObservableList<Node> radioButtons;
|
||||
private final ObservableList<Node> sortedRadioButtons;
|
||||
|
||||
private final String hubUrl;
|
||||
|
||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||
|
||||
//FXML
|
||||
public ToggleGroup locationPresetsToggler;
|
||||
public VBox radioButtonVBox;
|
||||
public HBox customLocationRadioBtn;
|
||||
public RadioButton customRadioButton;
|
||||
public Label locationStatusLabel;
|
||||
public FontAwesome5IconView goodLocation;
|
||||
public FontAwesome5IconView badLocation;
|
||||
|
||||
@Inject
|
||||
ImportTemplateLocationController(@ImportTemplateWindow Stage window, //
|
||||
@Named("vaultName") String vaultName, //
|
||||
VaultTemplate template, //
|
||||
ObjectProperty<Path> vaultPath, //
|
||||
@ImportTemplateWindow ObjectProperty<Vault> vault, //
|
||||
@FxmlScene(FxmlFile.IMPORT_TEMPLATE_SUCCESS) Lazy<Scene> successScene, //
|
||||
FxApplicationWindows appWindows, //
|
||||
VaultListManager vaultListManager, //
|
||||
ExecutorService executor, //
|
||||
Settings settings, //
|
||||
ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vaultName = vaultName;
|
||||
this.template = template;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vault = vault;
|
||||
this.successScene = successScene;
|
||||
this.appWindows = appWindows;
|
||||
this.vaultListManager = vaultListManager;
|
||||
this.executor = executor;
|
||||
this.settings = settings;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.hubUrl = Objects.requireNonNullElseGet(template.hubUrl(), () -> resourceBundle.getString("importTemplate.hubUrl.none"));
|
||||
this.vaultPathStatus = ObservableUtil.mapWithDefault(vaultPath, this::validatePath, new VaultPathStatus(false, "error.message"));
|
||||
this.validVaultPath = ObservableUtil.mapWithDefault(vaultPathStatus, VaultPathStatus::valid, false);
|
||||
this.vaultPathStatus.addListener(this::updateStatusLabel);
|
||||
this.usePresetPath = new SimpleBooleanProperty();
|
||||
this.readyToImport = Bindings.createBooleanBinding(() -> validVaultPath.getValue() && !processing.get(), validVaultPath, processing);
|
||||
this.createButtonState = Bindings.when(processing).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
this.radioButtons = FXCollections.observableArrayList();
|
||||
this.sortedRadioButtons = radioButtons.sorted(this::compareLocationPresets);
|
||||
|
||||
Path previouslyUsedDirectory = settings.previouslyUsedVaultDirectory.get();
|
||||
if (previouslyUsedDirectory != null) {
|
||||
try {
|
||||
if (Files.exists(previouslyUsedDirectory) && Files.isDirectory(previouslyUsedDirectory) && isActuallyWritable(previouslyUsedDirectory)) {
|
||||
this.customVaultPath = previouslyUsedDirectory;
|
||||
}
|
||||
} catch (InvalidPathException | NullPointerException e) {
|
||||
LOG.warn("Invalid previously used vault directory path: {}", previouslyUsedDirectory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VaultPathStatus validatePath(Path p) throws NullPointerException {
|
||||
if (!Files.exists(p.getParent())) {
|
||||
return new VaultPathStatus(false, "addvaultwizard.new.locationDoesNotExist");
|
||||
} else if (!isActuallyWritable(p.getParent())) {
|
||||
return new VaultPathStatus(false, "addvaultwizard.new.locationIsNotWritable");
|
||||
} else if (!Files.notExists(p)) {
|
||||
return new VaultPathStatus(false, "addvaultwizard.new.fileAlreadyExists");
|
||||
} else {
|
||||
return new VaultPathStatus(true, "addvaultwizard.new.locationIsOk");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatusLabel(ObservableValue<? extends VaultPathStatus> observable, VaultPathStatus oldValue, VaultPathStatus newValue) {
|
||||
if (newValue.valid()) {
|
||||
locationStatusLabel.setGraphic(goodLocation);
|
||||
locationStatusLabel.getStyleClass().remove("label-red");
|
||||
locationStatusLabel.getStyleClass().add("label-muted");
|
||||
} else {
|
||||
locationStatusLabel.setGraphic(badLocation);
|
||||
locationStatusLabel.getStyleClass().remove("label-muted");
|
||||
locationStatusLabel.getStyleClass().add("label-red");
|
||||
}
|
||||
this.locationStatusLabel.setText(resourceBundle.getString(newValue.localizationKey()));
|
||||
}
|
||||
|
||||
private boolean isActuallyWritable(Path p) {
|
||||
Path tmpDir = null;
|
||||
try {
|
||||
tmpDir = Files.createTempDirectory(p, TEMP_FILE_PREFIX);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (tmpDir != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tmpDir);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to delete temporary directory {}. Needs to be deleted manually.", tmpDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
var task = executor.submit(this::loadLocationPresets);
|
||||
window.addEventHandler(WindowEvent.WINDOW_HIDING, _ -> {
|
||||
task.cancel(true);
|
||||
template.close(); //discards the temporary extraction directory, whether or not the import completed
|
||||
});
|
||||
locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||
usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||
radioButtons.add(customLocationRadioBtn);
|
||||
Bindings.bindContent(radioButtonVBox.getChildren(), sortedRadioButtons); //to prevent garbage collection of the binding, we bind explicitly to the sorted list
|
||||
}
|
||||
|
||||
private void loadLocationPresets() {
|
||||
Platform.runLater(() -> loadingPresetLocations.set(true));
|
||||
try {
|
||||
LocationPresetsProvider.loadAll(LocationPresetsProvider.class) //
|
||||
.flatMap(LocationPresetsProvider::getLocations) //we do not use sorted(), because it evaluates the stream elements, blocking until all elements are gathered
|
||||
.forEach(this::createRadioButtonFor);
|
||||
} finally {
|
||||
Platform.runLater(() -> loadingPresetLocations.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
private void createRadioButtonFor(LocationPreset preset) {
|
||||
Platform.runLater(() -> {
|
||||
var btn = new RadioButton(preset.name());
|
||||
btn.setUserData(preset.path());
|
||||
radioButtons.add(btn);
|
||||
locationPresetsToggler.getToggles().add(btn);
|
||||
});
|
||||
}
|
||||
|
||||
private int compareLocationPresets(Node left, Node right) {
|
||||
if (customLocationRadioBtn.getId().equals(left.getId())) {
|
||||
return 1;
|
||||
} else if (customLocationRadioBtn.getId().equals(right.getId())) {
|
||||
return -1;
|
||||
} else {
|
||||
return ((RadioButton) left).getText().compareToIgnoreCase(((RadioButton) right).getText());
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||
var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath);
|
||||
vaultPath.set(storagePath.resolve(vaultName));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void chooseCustomVaultPath() {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
|
||||
if (Files.exists(customVaultPath)) {
|
||||
directoryChooser.setInitialDirectory(customVaultPath.toFile());
|
||||
} else {
|
||||
directoryChooser.setInitialDirectory(DEFAULT_CUSTOM_VAULT_PATH.toFile());
|
||||
}
|
||||
final File file = directoryChooser.showDialog(window);
|
||||
if (file != null) {
|
||||
customVaultPath = file.toPath().toAbsolutePath();
|
||||
vaultPath.set(customVaultPath.resolve(vaultName));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void finish() {
|
||||
if (!isReadyToImport()) {
|
||||
return;
|
||||
}
|
||||
Path destination = vaultPath.get();
|
||||
processing.set(true);
|
||||
// the template was already unpacked and validated when the deeplink arrived, so only the destination can still
|
||||
// fail here - which the location picker has just checked, leaving races and hardware faults
|
||||
Tasks.create(() -> {
|
||||
template.moveTo(destination);
|
||||
return vaultListManager.add(destination);
|
||||
}).onSuccess(newVault -> {
|
||||
vault.set(newVault);
|
||||
rememberParentDirectory(destination);
|
||||
window.setScene(successScene.get());
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to import vault template.", e);
|
||||
appWindows.showErrorWindow(e, window, window.getScene());
|
||||
}).andFinally(() -> processing.set(false)).runOnce(executor);
|
||||
}
|
||||
|
||||
private void rememberParentDirectory(Path destination) {
|
||||
Path parentPath = destination.getParent();
|
||||
if (parentPath != null) {
|
||||
settings.previouslyUsedVaultDirectory.setValue(parentPath);
|
||||
}
|
||||
}
|
||||
|
||||
/* Internal classes */
|
||||
|
||||
private record VaultPathStatus(boolean valid, String localizationKey) {
|
||||
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getVaultName() {
|
||||
return vaultName;
|
||||
}
|
||||
|
||||
public String getHubUrl() {
|
||||
return hubUrl;
|
||||
}
|
||||
|
||||
public Path getVaultPath() {
|
||||
return vaultPath.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Path> vaultPathProperty() {
|
||||
return vaultPath;
|
||||
}
|
||||
|
||||
public boolean isLoadingPresetLocations() {
|
||||
return loadingPresetLocations.getValue();
|
||||
}
|
||||
|
||||
public BooleanProperty loadingPresetLocationsProperty() {
|
||||
return loadingPresetLocations;
|
||||
}
|
||||
|
||||
public BooleanProperty usePresetPathProperty() {
|
||||
return usePresetPath;
|
||||
}
|
||||
|
||||
public boolean isUsePresetPath() {
|
||||
return usePresetPath.get();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> readyToImportProperty() {
|
||||
return readyToImport;
|
||||
}
|
||||
|
||||
public boolean isReadyToImport() {
|
||||
return readyToImport.getValue();
|
||||
}
|
||||
|
||||
public ObjectBinding<ContentDisplay> createButtonStateProperty() {
|
||||
return createButtonState;
|
||||
}
|
||||
|
||||
public ContentDisplay getCreateButtonState() {
|
||||
return createButtonState.get();
|
||||
}
|
||||
|
||||
public BooleanBinding anyRadioButtonSelectedProperty() {
|
||||
return locationPresetsToggler.selectedToggleProperty().isNotNull();
|
||||
}
|
||||
|
||||
public boolean isAnyRadioButtonSelected() {
|
||||
return anyRadioButtonSelectedProperty().get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.fxapp.PrimaryStage;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module
|
||||
abstract class ImportTemplateModule {
|
||||
|
||||
@Provides
|
||||
@ImportTemplateWindow
|
||||
@ImportTemplateScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ImportTemplateWindow
|
||||
@ImportTemplateScoped
|
||||
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(primaryStage);
|
||||
stage.setTitle(resourceBundle.getString("importTemplate.title"));
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ImportTemplateScoped
|
||||
static ObjectProperty<Path> provideVaultPath() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ImportTemplateWindow
|
||||
@ImportTemplateScoped
|
||||
static ObjectProperty<Vault> provideVault() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
// ------------------
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.IMPORT_TEMPLATE_LOCATION)
|
||||
@ImportTemplateScoped
|
||||
static Scene provideImportTemplateLocationScene(@ImportTemplateWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.IMPORT_TEMPLATE_LOCATION);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.IMPORT_TEMPLATE_SUCCESS)
|
||||
@ImportTemplateScoped
|
||||
static Scene provideImportTemplateSuccessScene(@ImportTemplateWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.IMPORT_TEMPLATE_SUCCESS);
|
||||
}
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ImportTemplateLocationController.class)
|
||||
abstract FxController bindImportTemplateLocationController(ImportTemplateLocationController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ImportTemplateSuccessController.class)
|
||||
abstract FxController bindImportTemplateSuccessController(ImportTemplateSuccessController controller);
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
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 ImportTemplateScoped {
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@ImportTemplateScoped
|
||||
public class ImportTemplateSuccessController implements FxController {
|
||||
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final Stage window;
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
|
||||
@Inject
|
||||
ImportTemplateSuccessController(FxApplicationWindows appWindows, @ImportTemplateWindow Stage window, @ImportTemplateWindow ObjectProperty<Vault> vault) {
|
||||
this.appWindows = appWindows;
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void unlockAndClose() {
|
||||
close();
|
||||
appWindows.startUnlockWorkflow(vault.get(), window);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Vault> vaultProperty() {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public Vault getVault() {
|
||||
return vault.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
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)
|
||||
@interface ImportTemplateWindow {
|
||||
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.cryptomator.ui.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.cryptomator.ui.fxapp.PrimaryStage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* Entry point for showing the vault template import flow.
|
||||
* <p>
|
||||
* Everything the flow needs to decide - unpacking the archive, and choosing between the import dialog and the
|
||||
* "malformed template" dialog.
|
||||
*/
|
||||
@FxApplicationScoped
|
||||
public class ImportTemplateWindows {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ImportTemplateWindows.class);
|
||||
|
||||
private final ImportTemplateComponent.Factory importTemplateWindow;
|
||||
private final Dialogs dialogs;
|
||||
private final Stage primaryStage;
|
||||
private final ExecutorService executor;
|
||||
|
||||
@Inject
|
||||
ImportTemplateWindows(ImportTemplateComponent.Factory importTemplateWindow, //
|
||||
Dialogs dialogs, //
|
||||
@PrimaryStage Stage primaryStage, //
|
||||
ExecutorService executor) {
|
||||
this.importTemplateWindow = importTemplateWindow;
|
||||
this.dialogs = dialogs;
|
||||
this.primaryStage = primaryStage;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks the given archive off the FX thread and shows the import dialog for it.
|
||||
* <p>
|
||||
* A template that could never be imported is rejected before the user is asked to choose a storage location: in that
|
||||
* case {@link #showMalformedTemplateDialog()} is shown instead and the returned stage is the primary stage. Failures
|
||||
* that are <em>not</em> the template's fault (the file system refusing to unpack it) complete the stage
|
||||
* exceptionally, leaving the generic error display to the caller.
|
||||
*
|
||||
* @param name the fixed vault name
|
||||
* @param archive the ZIP archive bytes
|
||||
* @return the import window, or the primary stage if the template was rejected
|
||||
*/
|
||||
public CompletionStage<Stage> extractAndShowImportTemplateWindow(String name, byte[] archive) {
|
||||
return extractAsync(archive)
|
||||
.handleAsync((template, throwable) -> {
|
||||
if (throwable != null) {
|
||||
var cause = throwable.getCause() != null ? throwable.getCause() : throwable;
|
||||
if (cause instanceof MalformedTemplateException) {
|
||||
// template itself is invalid
|
||||
LOG.error("Vault template is malformed.", cause);
|
||||
return showMalformedTemplateDialog();
|
||||
} else {
|
||||
LOG.error("Failed to unpack vault template.", cause);
|
||||
throw new CompletionException(cause);
|
||||
}
|
||||
}
|
||||
try {
|
||||
var component = importTemplateWindow.create(name, template);
|
||||
component.showImportTemplateWindow();
|
||||
return component.window();
|
||||
} catch (RuntimeException e) {
|
||||
template.close(); //nothing took ownership, so the temporary directory is ours to discard
|
||||
throw e;
|
||||
}
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async Wrapper for {@link VaultTemplate#extract(byte[])}
|
||||
* <p>
|
||||
* Failures arrive as a {@link CompletionException} wrapping the cause: a {@link MalformedTemplateException}
|
||||
* means the template itself is a dead end, any other {@link IOException} means unpacking failed for
|
||||
* an environmental reason.
|
||||
*
|
||||
* @param archive the ZIP archive bytes
|
||||
* @return the unpacked template, which the caller must close
|
||||
* @see VaultTemplate#extract(byte[])
|
||||
*/
|
||||
CompletionStage<VaultTemplate> extractAsync(byte[] archive) {
|
||||
return CompletableFuture.supplyAsync(()-> {
|
||||
try {
|
||||
return VaultTemplate.extract(archive);
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the user that the template cannot be imported. Must be called on the FX thread.
|
||||
*
|
||||
* @return the primary stage, which is what remains visible once the user dismisses the dialog
|
||||
*/
|
||||
public Stage showMalformedTemplateDialog() {
|
||||
dialogs.prepareMalformedTemplateDialog(primaryStage).build().showAndWait();
|
||||
return primaryStage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Indicates that a vault template cannot be imported because the archive itself is unusable - as opposed to an
|
||||
* {@link IOException} arising from the destination (already exists, not writable, ...).
|
||||
* <p>
|
||||
* Callers distinguish the two to decide what to show the user: a malformed template is a dead end, whereas a
|
||||
* destination problem is recoverable by picking a different location.
|
||||
*/
|
||||
public class MalformedTemplateException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MalformedTemplateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MalformedTemplateException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||
import org.cryptomator.ui.keyloading.hub.HubConfig;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* A vault template that has been unpacked to a temporary directory and found sound.
|
||||
* <p>
|
||||
* Holding an instance means the template <em>is</em> valid: it was a readable ZIP within the entry and size
|
||||
* limits, held a decodable {@value Constants#VAULTCONFIG_FILENAME} directly in its root and no entry escaped
|
||||
* the extraction directory.
|
||||
* <p>
|
||||
* Because the vault lies in the archive root, the temporary directory <em>is</em> the vault directory: importing moves
|
||||
* it wholesale to its destination.
|
||||
* <p>
|
||||
* The instance owns that temporary directory and <strong>must be {@link #close() closed}</strong>, whether or not the
|
||||
* import completes.
|
||||
* <p>
|
||||
* {@link #hubUrl()} is read from the <em>unverified</em> vault config.
|
||||
*/
|
||||
public final class VaultTemplate implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultTemplate.class);
|
||||
|
||||
private final Path vaultDir;
|
||||
private final @Nullable String hubUrl;
|
||||
|
||||
private VaultTemplate(Path vaultDir, @Nullable String hubUrl) {
|
||||
this.vaultDir = vaultDir;
|
||||
this.hubUrl = hubUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks the given archive to a temporary directory and validates it.
|
||||
* <p>
|
||||
* This touches the file system and should not run on the FX thread.
|
||||
*
|
||||
* @param archive the ZIP archive bytes
|
||||
* @return the unpacked template, which the caller must close
|
||||
* @throws MalformedTemplateException if the archive is unusable
|
||||
* @throws IOException if unpacking fails
|
||||
*/
|
||||
public static VaultTemplate extract(byte[] archive) throws IOException {
|
||||
return extract(archive, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static VaultTemplate extract(byte[] archive, @Nullable Path tempParent) throws IOException {
|
||||
Path vaultDir = tempParent == null //
|
||||
? Files.createTempDirectory("vault-template-") //
|
||||
: Files.createTempDirectory(tempParent, "vault-template-");
|
||||
try {
|
||||
VaultTemplateExtractor.extractTo(archive, vaultDir);
|
||||
var hubUrl = readHubUrl(vaultDir);
|
||||
return new VaultTemplate(vaultDir, hubUrl);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
VaultTemplateExtractor.deleteQuietly(vaultDir);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable String readHubUrl(Path vaultDir) throws IOException {
|
||||
var token = Files.readString(vaultDir.resolve(Constants.VAULTCONFIG_FILENAME), StandardCharsets.US_ASCII).trim();
|
||||
HubConfig hubConfig;
|
||||
try {
|
||||
hubConfig = VaultConfig.decode(token).getHeader("hub", HubConfig.class);
|
||||
} catch (VaultConfigLoadException e) {
|
||||
throw new MalformedTemplateException("Template does not contain a decodable vault config.", e);
|
||||
}
|
||||
if (hubConfig == null) {
|
||||
return null; // not a hub vault - allowed, the user just cannot be shown a Hub
|
||||
}
|
||||
try {
|
||||
return hubConfig.getApiBaseUrl().toString();
|
||||
} catch (RuntimeException e) {
|
||||
// hub header present but unusable (e.g. neither apiBaseUrl nor devicesResourceUrl set)
|
||||
LOG.warn("Vault template declares an unusable hub config.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hub instance which generated the template, or {@code null} if its config declares none.
|
||||
* <p>
|
||||
* Unverified - see the class documentation.
|
||||
*/
|
||||
public @Nullable String hubUrl() {
|
||||
return hubUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the unpacked vault to its final location.
|
||||
*
|
||||
* @param destination the target vault directory, which must not yet exist
|
||||
* @throws java.nio.file.FileAlreadyExistsException if {@code destination} already exists
|
||||
* @throws IOException if the vault cannot be moved
|
||||
*/
|
||||
public void moveTo(Path destination) throws IOException {
|
||||
VaultTemplateExtractor.moveToDestination(vaultDir, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the unpacked vault. A no-op after a successful {@link #moveTo(Path)}, which relocates the directory
|
||||
* this would otherwise delete.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
VaultTemplateExtractor.deleteQuietly(vaultDir);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
/**
|
||||
* Low-level mechanics of unpacking a vault template (a ZIP archive holding a ready-made vault).
|
||||
* <p>
|
||||
* Per the template spec the vault lies directly in the archive root, i.e. the archive holds
|
||||
* {@value Constants#VAULTCONFIG_FILENAME} at its top level rather than inside an enclosing folder. The extraction
|
||||
* directory therefore <em>is</em> the vault directory. Moving it to its final location is a separate step, so the vault
|
||||
* only ever appears complete at its destination.
|
||||
* <p>
|
||||
* Lifecycle of the temporary directory is owned by {@link VaultTemplate}, not by this class.
|
||||
*/
|
||||
final class VaultTemplateExtractor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultTemplateExtractor.class);
|
||||
// On Windows, URI template (base64url encoded) is is given on command line argument. Windows API restrict the length
|
||||
// to 32,767 chars (approx. ~24KB template size).
|
||||
// Note that larger links are dropped by the OS anyway, but we also set the accepted size uniform across platforms
|
||||
static final long MAX_TOTAL_BYTES = 24_000; // ~24 KB, see above
|
||||
static final int MAX_ENTRIES = 30; // files + directories below the zip root
|
||||
|
||||
private VaultTemplateExtractor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks the given template into {@code targetDir}, which then holds the vault itself.
|
||||
*
|
||||
* @param template the ZIP archive bytes
|
||||
* @param targetDir an existing, empty directory to unpack into
|
||||
* @throws MalformedTemplateException if the template is not a readable ZIP, exceeds the entry or size limits, or
|
||||
* does not hold {@value Constants#VAULTCONFIG_FILENAME} in its root
|
||||
* @throws IOException if the files cannot be written
|
||||
*/
|
||||
static void extractTo(byte[] template, Path targetDir) throws IOException {
|
||||
Path tmpZip = Files.createTempFile("vault-template-", ".zip");
|
||||
try {
|
||||
Files.write(tmpZip, template);
|
||||
unzip(tmpZip, targetDir);
|
||||
} finally {
|
||||
deleteQuietly(tmpZip);
|
||||
}
|
||||
}
|
||||
|
||||
private static void unzip(Path zipFile, Path targetDir) throws IOException {
|
||||
Path normalizedTarget = targetDir.normalize();
|
||||
try (FileSystem zipFs = FileSystems.newFileSystem(zipFile)) {
|
||||
Path zipRoot = zipFs.getRootDirectories().iterator().next(); //we take the first available root and ignore others
|
||||
Files.walkFileTree(zipRoot, new TemplateExtractionVisitor(zipRoot, normalizedTarget, MAX_ENTRIES, MAX_TOTAL_BYTES));
|
||||
} catch (ZipException e) {
|
||||
throw new MalformedTemplateException("Template is not a readable ZIP archive.", e);
|
||||
}
|
||||
if (!Files.isRegularFile(normalizedTarget.resolve(Constants.VAULTCONFIG_FILENAME))) {
|
||||
throw new MalformedTemplateException("Template does not hold " + Constants.VAULTCONFIG_FILENAME + " in its root directory.");
|
||||
}
|
||||
}
|
||||
|
||||
static class TemplateExtractionVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
private final Path zipRoot;
|
||||
private final Path target;
|
||||
private final int maxEntries;
|
||||
private final long maxSize;
|
||||
|
||||
private int totalEntries = 0;
|
||||
private long totalBytes = 0;
|
||||
|
||||
TemplateExtractionVisitor(Path zipRoot, Path target, int maxEntries, long maxSize) {
|
||||
this.zipRoot = zipRoot;
|
||||
this.target = target;
|
||||
this.maxEntries = maxEntries;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (!dir.equals(zipRoot)) { //the zip root is not part of the template's content
|
||||
countEntry();
|
||||
}
|
||||
Files.createDirectories(resolveSafely(target, zipRoot, dir));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
countEntry();
|
||||
totalBytes += attrs.size();
|
||||
if (totalBytes > maxSize) {
|
||||
throw new MalformedTemplateException("Vault template exceeds the maximum allowed size of " + maxSize + " bytes.");
|
||||
}
|
||||
|
||||
Files.copy(file, resolveSafely(target, zipRoot, file), StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
private void countEntry() throws IOException {
|
||||
totalEntries++;
|
||||
if ( totalEntries > maxEntries) {
|
||||
throw new MalformedTemplateException("Vault template contains more than the maximum allowed " + maxEntries + " entries.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path resolveSafely(Path targetDir, Path zipRoot, Path entry) throws IOException {
|
||||
Path resolved = targetDir.resolve(zipRoot.relativize(entry).toString()).normalize();
|
||||
if (!resolved.startsWith(targetDir)) {
|
||||
throw new MalformedTemplateException("Refusing to extract entry outside of target directory: " + entry);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an unpacked vault to its final location, creating missing parent directories.
|
||||
*
|
||||
* @param vaultRoot the unpacked vault directory
|
||||
* @param destination the target vault directory, which must not yet exist
|
||||
* @throws FileAlreadyExistsException if {@code destination} already exists
|
||||
*/
|
||||
static void moveToDestination(Path vaultRoot, Path destination) throws IOException {
|
||||
if (Files.exists(destination)) {
|
||||
throw new FileAlreadyExistsException(destination.toString());
|
||||
}
|
||||
Path parent = destination.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
move(vaultRoot, destination);
|
||||
}
|
||||
|
||||
private static void move(Path source, Path destination) throws IOException {
|
||||
try {
|
||||
Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE);
|
||||
return;
|
||||
} catch (AtomicMoveNotSupportedException | UnsupportedOperationException e) {
|
||||
// provider without atomic move support - fall through to non-atomic strategies
|
||||
}
|
||||
try {
|
||||
Files.move(source, destination);
|
||||
} catch (IOException e) {
|
||||
// likely a cross-store move of a non-empty directory: copy recursively, then delete the source
|
||||
copyRecursively(source, destination);
|
||||
deleteRecursively(source);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyRecursively(Path source, Path target) throws IOException {
|
||||
Files.walkFileTree(source, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
Files.createDirectories(target.resolve(source.relativize(dir).toString()));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.copy(file, target.resolve(source.relativize(file).toString()), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void deleteQuietly(Path path) {
|
||||
try {
|
||||
deleteRecursively(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to clean up temporary path {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursively(Path path) throws IOException {
|
||||
if (!Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> stream = Files.walk(path)) {
|
||||
var paths = stream.sorted(Comparator.reverseOrder()).toList();
|
||||
for (Path p : paths) {
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.RadioButton?>
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.ToggleGroup?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.importtemplate.ImportTemplateLocationController"
|
||||
prefWidth="450"
|
||||
prefHeight="450"
|
||||
spacing="12"
|
||||
alignment="CENTER_LEFT"
|
||||
accessibleRole="DIALOG">
|
||||
<fx:define>
|
||||
<ToggleGroup fx:id="locationPresetsToggler"/>
|
||||
<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES"/>
|
||||
<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%importTemplate.hubUrlLabel" labelFor="$hubUrlTextField"/>
|
||||
<TextField fx:id="hubUrlTextField" text="${controller.hubUrl}" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%importTemplate.nameLabel" labelFor="$vaultNameTextField"/>
|
||||
<TextField fx:id="vaultNameTextField" text="${controller.vaultName}" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
|
||||
<Region prefHeight="6" VBox.vgrow="NEVER"/>
|
||||
|
||||
<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
|
||||
<ScrollPane hbarPolicy="NEVER">
|
||||
<VBox fx:id="radioButtonVBox" spacing="6">
|
||||
<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
|
||||
<HBox fx:id="customLocationRadioBtn" spacing="12" alignment="CENTER_LEFT">
|
||||
<RadioButton fx:id="customRadioButton" toggleGroup="${locationPresetsToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
|
||||
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="FOLDER_OPEN"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
<Label wrapText="true" text="%addvaultwizard.new.locationLoading" visible="${controller.loadingPresetLocations}" managed="${controller.loadingPresetLocations}" graphicTextGap="8">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner/>
|
||||
</graphic>
|
||||
</Label>
|
||||
|
||||
<Region prefHeight="12" VBox.vgrow="NEVER"/>
|
||||
|
||||
<VBox spacing="6">
|
||||
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
|
||||
<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
|
||||
<Label fx:id="locationStatusLabel" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6"/>
|
||||
</VBox>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+X">
|
||||
<buttons>
|
||||
<Button text="%importTemplate.createVaultBtn" ButtonBar.buttonData="NEXT_FORWARD" onAction="#finish" defaultButton="true" disable="${!controller.readyToImport}"
|
||||
contentDisplay="${controller.createButtonState}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.importtemplate.ImportTemplateSuccessController"
|
||||
prefWidth="450"
|
||||
prefHeight="450"
|
||||
spacing="12"
|
||||
alignment="TOP_CENTER"
|
||||
accessibleRole="DIALOG">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
|
||||
<Circle styleClass="glyph-icon-primary" radius="36"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="36"/>
|
||||
</StackPane>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<FormattedLabel format="%importTemplate.success.message" arg1="${controller.vault.displayName}" wrapText="true" HBox.hgrow="ALWAYS"/>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+IU">
|
||||
<buttons>
|
||||
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" onAction="#close" defaultButton="${!controller.vault.locked}"/>
|
||||
<Button text="%importTemplate.success.unlockNow" ButtonBar.buttonData="OTHER" onAction="#unlockAndClose" defaultButton="${controller.vault.locked}" visible="${controller.vault.locked}"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -667,17 +667,6 @@ retryIfReadonly.message=No write access to vault directory
|
||||
retryIfReadonly.description=Cryptomator cannot write to the vault directory. You can change the vault to be read-only and try again. This option can be disabled in the vault options.
|
||||
retryIfReadonly.retry=Change and Retry
|
||||
|
||||
# Import Template
|
||||
importTemplate.title=Setup Vault
|
||||
importTemplate.hubUrlLabel=Hub
|
||||
importTemplate.hubUrl.none=None
|
||||
importTemplate.nameLabel=Vault name
|
||||
importTemplate.createVaultBtn=Setup Vault
|
||||
importTemplate.malformedTemplate.message=Invalid vault template
|
||||
importTemplate.malformedTemplate.description=The linked vault template is damaged or unsupported. No vault has been created. Please download and import the vault template manually.
|
||||
importTemplate.success.message=Vault %s has been setup.
|
||||
importTemplate.success.unlockNow=Unlock Now
|
||||
|
||||
# Share Vault
|
||||
shareVault.title=Share Vault
|
||||
shareVault.message=Would you like to share your vault with others?
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.cryptomator.JavaFXUtil;
|
||||
import org.cryptomator.ui.dialogs.Dialogs;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class ImportTemplateWindowsTest {
|
||||
|
||||
private static final byte[] CIPHERTEXT = "some-ciphertext".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String API_BASE_URL = "https://hub.example.com/api";
|
||||
|
||||
private ExecutorService executor;
|
||||
private Stage primaryStage;
|
||||
private ImportTemplateWindows inTest;
|
||||
|
||||
@BeforeAll
|
||||
public static void initJavaFx() throws InterruptedException {
|
||||
// extractAndShowImportTemplateWindow completes on the FX thread
|
||||
var isRunning = JavaFXUtil.startPlatform();
|
||||
Assumptions.assumeTrue(isRunning);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
primaryStage = mock(Stage.class);
|
||||
inTest = spy(new ImportTemplateWindows(mock(ImportTemplateComponent.Factory.class), mock(Dialogs.class), primaryStage, executor));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("extractAsync yields the unpacked template")
|
||||
public void testExtractAsync() throws Exception {
|
||||
var zip = zip(Map.of("vault.cryptomator", hubVaultConfig()));
|
||||
|
||||
var template = inTest.extractAsync(zip).toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
|
||||
try (template) {
|
||||
Assertions.assertEquals(API_BASE_URL + "/", template.hubUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("extractAsync hands a dependent stage a CompletionException wrapping the malformed cause")
|
||||
public void testExtractAsyncWrapsMalformedCause() throws Exception {
|
||||
var zip = zip(Map.of("readme.txt", CIPHERTEXT)); // no vault config
|
||||
|
||||
// this wrapping is load-bearing: extractAndShowImportTemplateWindow unwraps exactly one layer to decide
|
||||
// between the malformed dialog and the generic error window
|
||||
var seen = inTest.extractAsync(zip) //
|
||||
.handle((template, throwable) -> throwable) //
|
||||
.toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
|
||||
Assertions.assertInstanceOf(CompletionException.class, seen);
|
||||
Assertions.assertInstanceOf(MalformedTemplateException.class, seen.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a malformed template is reported by the malformed-template dialog")
|
||||
public void testMalformedTemplateShowsDialog() throws Exception {
|
||||
var zip = zip(Map.of("readme.txt", CIPHERTEXT)); // no vault config
|
||||
doReturn(primaryStage).when(inTest).showMalformedTemplateDialog();
|
||||
|
||||
var result = inTest.extractAndShowImportTemplateWindow("MyVault", zip).toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
|
||||
verify(inTest).showMalformedTemplateDialog();
|
||||
Assertions.assertEquals(primaryStage, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a failure that is not the template's fault does not claim the template is malformed")
|
||||
public void testEnvironmentalFailureIsNotReportedAsMalformed() {
|
||||
var cause = new IOException("no space left on device");
|
||||
doReturn(CompletableFuture.failedFuture(new CompletionException(cause))).when(inTest).extractAsync(org.mockito.ArgumentMatchers.any());
|
||||
|
||||
var future = inTest.extractAndShowImportTemplateWindow("MyVault", new byte[0]).toCompletableFuture();
|
||||
|
||||
var thrown = Assertions.assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
|
||||
Assertions.assertEquals(cause, thrown.getCause());
|
||||
verify(inTest, never()).showMalformedTemplateDialog();
|
||||
}
|
||||
|
||||
private static byte[] hubVaultConfig() {
|
||||
var header = """
|
||||
{"kid":"hub+https://hub.example.com/api/vaults/123","typ":"JWT","alg":"HS256",\
|
||||
"hub":{"clientId":"cryptomator","authEndpoint":"https://login.example.com/auth",\
|
||||
"tokenEndpoint":"https://login.example.com/token",\
|
||||
"authSuccessUrl":"https://hub.example.com/app/unlock-success",\
|
||||
"authErrorUrl":"https://hub.example.com/app/unlock-error",\
|
||||
"apiBaseUrl":"%s"}}""".formatted(API_BASE_URL);
|
||||
var payload = "{\"format\":8,\"cipherCombo\":\"SIV_GCM\",\"shorteningThreshold\":220}";
|
||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
var token = encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) //
|
||||
+ "." + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) //
|
||||
+ "." + encoder.encodeToString("signature".getBytes(StandardCharsets.UTF_8));
|
||||
return token.getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static byte[] zip(Map<String, byte[]> entries) throws IOException {
|
||||
var ordered = new LinkedHashMap<>(entries);
|
||||
var baos = new ByteArrayOutputStream();
|
||||
try (var zos = new ZipOutputStream(baos)) {
|
||||
for (var entry : ordered.entrySet()) {
|
||||
zos.putNextEntry(new ZipEntry(entry.getKey()));
|
||||
zos.write(entry.getValue());
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class VaultTemplateExtractorTest {
|
||||
|
||||
private static final byte[] CONFIG = "hub-vault-config".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CIPHERTEXT = "some-ciphertext".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Test
|
||||
@DisplayName("a vault at the archive root is unpacked into the target directory")
|
||||
public void testVaultAtArchiveRoot(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", CONFIG, //
|
||||
"d/AB/CDEF/0.c9r", CIPHERTEXT //
|
||||
));
|
||||
|
||||
VaultTemplateExtractor.extractTo(zip, tmp);
|
||||
|
||||
Assertions.assertArrayEquals(CONFIG, Files.readAllBytes(tmp.resolve("vault.cryptomator")));
|
||||
Assertions.assertArrayEquals(CIPHERTEXT, Files.readAllBytes(tmp.resolve("d/AB/CDEF/0.c9r")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a vault nested in a top-level folder fails as malformed")
|
||||
public void testVaultInSubfolder(@TempDir Path tmp) throws IOException {
|
||||
// requirement: the vault starts in the archive root, an enclosing folder is not a supported template
|
||||
var zip = zip(Map.of( //
|
||||
"TemplateVault/vault.cryptomator", CONFIG, //
|
||||
"TemplateVault/d/AB/CDEF/0.c9r", CIPHERTEXT //
|
||||
));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractTo(zip, tmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("data that is not a zip archive fails as malformed")
|
||||
public void testNotAZip(@TempDir Path tmp) {
|
||||
var notAZip = "this is not a zip archive".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractTo(notAZip, tmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive without a vault config fails as malformed")
|
||||
public void testNoVaultConfig(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of("readme.txt", CONFIG));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractTo(zip, tmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a further vault config below the root is ordinary content, the root one identifies the vault")
|
||||
public void testFurtherVaultConfigBelowRoot(@TempDir Path tmp) throws IOException {
|
||||
// with the vault fixed at the archive root a nested config is simply extracted along with everything
|
||||
// else and counts against the entry limit
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", CONFIG, //
|
||||
"nested/vault.cryptomator", CIPHERTEXT //
|
||||
));
|
||||
|
||||
VaultTemplateExtractor.extractTo(zip, tmp);
|
||||
|
||||
Assertions.assertArrayEquals(CONFIG, Files.readAllBytes(tmp.resolve("vault.cryptomator")));
|
||||
Assertions.assertArrayEquals(CIPHERTEXT, Files.readAllBytes(tmp.resolve("nested/vault.cryptomator")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive exceeding the size limit fails as malformed")
|
||||
public void testExceedsSizeLimit(@TempDir Path tmp) throws IOException {
|
||||
var big = new byte[(int) VaultTemplateExtractor.MAX_TOTAL_BYTES + 1];
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", CONFIG, //
|
||||
"d/AB/CDEF/0.c9r", big //
|
||||
));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractTo(zip, tmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive with exactly the maximum number of entries is accepted")
|
||||
public void testAtEntryLimit(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(flatEntries(VaultTemplateExtractor.MAX_ENTRIES));
|
||||
|
||||
VaultTemplateExtractor.extractTo(zip, tmp);
|
||||
|
||||
Assertions.assertArrayEquals(CONFIG, Files.readAllBytes(tmp.resolve("vault.cryptomator")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive exceeding the entry-count limit fails as malformed")
|
||||
public void testExceedsEntryLimit(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(flatEntries(VaultTemplateExtractor.MAX_ENTRIES + 1));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractTo(zip, tmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a zip-slip entry does not escape the extraction directory")
|
||||
public void testZipSlip(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", CONFIG, //
|
||||
"../escaped.txt", CIPHERTEXT //
|
||||
));
|
||||
var targetDir = tmp.resolve("nested");
|
||||
Files.createDirectory(targetDir);
|
||||
|
||||
try {
|
||||
VaultTemplateExtractor.extractTo(zip, targetDir);
|
||||
} catch (IOException e) {
|
||||
// acceptable: extraction refused the malicious entry
|
||||
}
|
||||
|
||||
Assertions.assertTrue(Files.notExists(tmp.resolve("escaped.txt")), "entry escaped the extraction directory");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an unpacked vault is moved to the destination, creating missing parents")
|
||||
public void testMoveToDestination(@TempDir Path tmp) throws IOException {
|
||||
var source = unpackedVault(tmp.resolve("unpacked"));
|
||||
var destination = tmp.resolve("parent").resolve("MyVault"); // parent does not exist yet
|
||||
|
||||
VaultTemplateExtractor.moveToDestination(source, destination);
|
||||
|
||||
Assertions.assertArrayEquals(CONFIG, Files.readAllBytes(destination.resolve("vault.cryptomator")));
|
||||
Assertions.assertArrayEquals(CIPHERTEXT, Files.readAllBytes(destination.resolve("d/AB/0.c9r")));
|
||||
Assertions.assertTrue(Files.notExists(source), "the vault should no longer be at its old location");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an existing destination is not overwritten")
|
||||
public void testMoveToExistingDestination(@TempDir Path tmp) throws IOException {
|
||||
var source = unpackedVault(tmp.resolve("unpacked"));
|
||||
var destination = Files.createDirectory(tmp.resolve("MyVault"));
|
||||
|
||||
Assertions.assertThrows(FileAlreadyExistsException.class, () -> VaultTemplateExtractor.moveToDestination(source, destination));
|
||||
}
|
||||
|
||||
private static Path unpackedVault(Path dir) throws IOException {
|
||||
Files.createDirectories(dir.resolve("d/AB"));
|
||||
Files.write(dir.resolve("vault.cryptomator"), CONFIG);
|
||||
Files.write(dir.resolve("d/AB/0.c9r"), CIPHERTEXT);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param count total number of entries below the zip root, the vault config included
|
||||
*/
|
||||
private static Map<String, byte[]> flatEntries(int count) {
|
||||
var entries = new LinkedHashMap<String, byte[]>();
|
||||
entries.put("vault.cryptomator", CONFIG);
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
entries.put("file" + i + ".c9r", CIPHERTEXT);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static byte[] zip(Map<String, byte[]> entries) throws IOException {
|
||||
var ordered = new LinkedHashMap<>(entries);
|
||||
var baos = new ByteArrayOutputStream();
|
||||
try (var zos = new ZipOutputStream(baos)) {
|
||||
for (var entry : ordered.entrySet()) {
|
||||
zos.putNextEntry(new ZipEntry(entry.getKey()));
|
||||
zos.write(entry.getValue());
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package org.cryptomator.ui.importtemplate;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class VaultTemplateTest {
|
||||
|
||||
private static final byte[] CIPHERTEXT = "some-ciphertext".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String API_BASE_URL = "https://hub.example.com/api";
|
||||
|
||||
@Test
|
||||
@DisplayName("a hub template exposes the hub it belongs to")
|
||||
public void testHubUrl() throws IOException {
|
||||
var zip = zip(Map.of("vault.cryptomator", hubVaultConfig(API_BASE_URL)));
|
||||
|
||||
try (var inTest = VaultTemplate.extract(zip)) {
|
||||
Assertions.assertEquals(API_BASE_URL + "/", inTest.hubUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a template whose config declares no hub has no hub url")
|
||||
public void testNoHubHeader() throws IOException {
|
||||
var zip = zip(Map.of("vault.cryptomator", vaultConfig(null)));
|
||||
|
||||
try (var inTest = VaultTemplate.extract(zip)) {
|
||||
Assertions.assertNull(inTest.hubUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive whose vault config is not a decodable token is rejected")
|
||||
public void testUndecodableVaultConfig() throws IOException {
|
||||
var zip = zip(Map.of("vault.cryptomator", "not-a-jwt".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplate.extract(zip));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("closing discards the temporary directory, also after a completed import")
|
||||
public void testCloseCleansUpAfterMove(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", hubVaultConfig(API_BASE_URL), //
|
||||
"d/AB/CDEF/0.c9r", CIPHERTEXT //
|
||||
));
|
||||
var workDir = Files.createDirectory(tmp.resolve("work"));
|
||||
var destination = tmp.resolve("MyVault");
|
||||
|
||||
try (var inTest = VaultTemplate.extract(zip, workDir)) {
|
||||
Assertions.assertFalse(isEmpty(workDir), "sanity: the temporary directory should exist before close()");
|
||||
inTest.moveTo(destination);
|
||||
}
|
||||
|
||||
Assertions.assertTrue(isEmpty(workDir), "temporary extraction directory was not cleaned up");
|
||||
Assertions.assertTrue(Files.exists(destination.resolve("vault.cryptomator")), "the moved vault must survive close()");
|
||||
Assertions.assertArrayEquals(CIPHERTEXT, Files.readAllBytes(destination.resolve("d/AB/CDEF/0.c9r")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("closing without importing discards the temporary directory")
|
||||
public void testCloseCleansUpWithoutMove(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of("vault.cryptomator", hubVaultConfig(API_BASE_URL)));
|
||||
var workDir = Files.createDirectory(tmp.resolve("work"));
|
||||
|
||||
try (var _ = VaultTemplate.extract(zip, workDir)) {
|
||||
Assertions.assertFalse(isEmpty(workDir), "sanity: the temporary directory should exist before close()");
|
||||
}
|
||||
|
||||
Assertions.assertTrue(isEmpty(workDir), "temporary extraction directory was not cleaned up");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a rejected template leaves no temporary directory behind")
|
||||
public void testNoTempDirLeakOnFailure(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of("readme.txt", CIPHERTEXT)); // no vault config
|
||||
var workDir = Files.createDirectory(tmp.resolve("work"));
|
||||
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplate.extract(zip, workDir));
|
||||
|
||||
Assertions.assertTrue(isEmpty(workDir), "a failed extraction leaked a temporary directory");
|
||||
}
|
||||
|
||||
private static boolean isEmpty(Path dir) throws IOException {
|
||||
try (var children = Files.list(dir)) {
|
||||
return children.findAny().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] hubVaultConfig(String apiBaseUrl) {
|
||||
return vaultConfig("""
|
||||
,
|
||||
"hub": {
|
||||
"clientId":"cryptomator",\
|
||||
"authEndpoint":"https://login.example.com/auth",\
|
||||
"tokenEndpoint":"https://login.example.com/token",\
|
||||
"authSuccessUrl":"https://hub.example.com/app/unlock-success",\
|
||||
"authErrorUrl":"https://hub.example.com/app/unlock-error",\
|
||||
"apiBaseUrl":"%s"
|
||||
}""".formatted(apiBaseUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a vault config token. The signature is not verifiable without the masterkey, which is exactly the state
|
||||
* the import dialog operates in - so a dummy signature is sufficient here.
|
||||
*/
|
||||
private static byte[] vaultConfig(String extraHeaderFields) {
|
||||
var header = """
|
||||
{ "kid":"hub+https://hub.example.com/api/vaults/123",\
|
||||
"typ":"JWT",\
|
||||
"alg":"HS256"\
|
||||
%s
|
||||
}""".formatted(extraHeaderFields == null ? "" : extraHeaderFields);
|
||||
var payload = """
|
||||
{ "format":8,\
|
||||
"cipherCombo":"SIV_GCM",\
|
||||
"shorteningThreshold":220\
|
||||
}""";
|
||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
var token = encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) //
|
||||
+ "." + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) //
|
||||
+ "." + encoder.encodeToString("signature".getBytes(StandardCharsets.UTF_8));
|
||||
return token.getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static byte[] zip(Map<String, byte[]> entries) throws IOException {
|
||||
var ordered = new LinkedHashMap<>(entries);
|
||||
var baos = new ByteArrayOutputStream();
|
||||
try (var zos = new ZipOutputStream(baos)) {
|
||||
for (var entry : ordered.entrySet()) {
|
||||
zos.putNextEntry(new ZipEntry(entry.getKey()));
|
||||
zos.write(entry.getValue());
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user