Remove VaultTemplate stuff

including jdk.zipfs as dependency in jlink

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
Armin Schrenk
2026-08-26 17:08:29 +02:00
parent 27eb6298ae
commit b01478f0fa
30 changed files with 10 additions and 1658 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 \
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 \
+1 -1
View File
@@ -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 \
+1 -1
View File
@@ -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);
}
}
@@ -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();
}
}