mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-23 23:56:02 +00:00
Extract zip once
and show error if extraction fails Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
@@ -10,6 +10,8 @@ import org.cryptomator.ui.dialogs.SimpleDialog;
|
||||
import org.cryptomator.ui.error.ErrorComponent;
|
||||
import org.cryptomator.ui.eventview.EventViewComponent;
|
||||
import org.cryptomator.ui.importtemplate.ImportTemplateComponent;
|
||||
import org.cryptomator.ui.importtemplate.MalformedTemplateException;
|
||||
import org.cryptomator.ui.importtemplate.VaultTemplate;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.notification.NotificationComponent;
|
||||
@@ -38,6 +40,7 @@ import java.awt.desktop.AppReopenedListener;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
@@ -148,12 +151,51 @@ public class FxApplicationWindows {
|
||||
CompletableFuture.runAsync(() -> shareVaultWindow.create(vault).showShareVaultWindow(), Platform::runLater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the import dialog for the given vault template.
|
||||
* <p>
|
||||
* The archive is unpacked and validated up front on a background thread, so a template that could never be imported
|
||||
* is rejected before the user is asked to choose a storage location - in which case the returned stage is the main
|
||||
* window, having shown an error dialog. On success the unpacked template is handed to the import window, which owns
|
||||
* it from then on and discards it when it closes.
|
||||
*/
|
||||
public CompletionStage<Stage> showImportTemplateWindow(String name, byte[] template) {
|
||||
return showMainWindow().thenApplyAsync(_ -> {
|
||||
var component = importTemplateWindow.create(name, template);
|
||||
component.showImportTemplateWindow();
|
||||
return component.window();
|
||||
}, Platform::runLater).whenComplete(this::reportErrors);
|
||||
return showMainWindow().thenComposeAsync(mainWindow -> //
|
||||
VaultTemplate.extractAsync(template, executor) //
|
||||
.handleAsync((extractedTemplate, throwable) -> {
|
||||
if (throwable != null) {
|
||||
return reportFailedImport(unwrap(throwable), mainWindow);
|
||||
}
|
||||
try {
|
||||
var component = importTemplateWindow.create(name, extractedTemplate);
|
||||
component.showImportTemplateWindow();
|
||||
return component.window();
|
||||
} catch (RuntimeException e) {
|
||||
extractedTemplate.close(); //nothing took ownership, so the temp dir is ours to discard
|
||||
throw e;
|
||||
}
|
||||
}, Platform::runLater) //
|
||||
).whenComplete(this::reportErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the main window, which is what remains visible once the user dismisses the error
|
||||
*/
|
||||
private Stage reportFailedImport(Throwable cause, Stage mainWindow) {
|
||||
if (cause instanceof MalformedTemplateException) {
|
||||
// a dead end: no storage location the user could pick would make this template work
|
||||
LOG.error("Vault template is malformed.", cause);
|
||||
dialogs.prepareMalformedTemplateDialog(mainWindow).build().showAndWait();
|
||||
} else {
|
||||
LOG.error("Failed to unpack vault template.", cause);
|
||||
showErrorWindow(cause, mainWindow, null);
|
||||
}
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
private static Throwable unwrap(Throwable throwable) {
|
||||
var cause = throwable.getCause();
|
||||
return throwable instanceof CompletionException && cause != null ? cause : throwable;
|
||||
}
|
||||
|
||||
public CompletionStage<Stage> showVaultOptionsWindow(Vault vault, SelectedVaultOptionsTab tab) {
|
||||
|
||||
@@ -28,7 +28,7 @@ public interface ImportTemplateComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
ImportTemplateComponent create(@BindsInstance @Named("vaultName") String name, @BindsInstance @Named("vaultTemplate") byte[] template);
|
||||
ImportTemplateComponent create(@BindsInstance @Named("vaultName") String name, @BindsInstance VaultTemplate template);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-15
@@ -12,7 +12,6 @@ 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.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -47,6 +46,7 @@ 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;
|
||||
@@ -60,7 +60,7 @@ public class ImportTemplateLocationController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final String vaultName;
|
||||
private final byte[] template;
|
||||
private final VaultTemplate template;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final ObjectProperty<Vault> vault;
|
||||
private final Lazy<Scene> successScene;
|
||||
@@ -69,7 +69,6 @@ public class ImportTemplateLocationController implements FxController {
|
||||
private final ExecutorService executor;
|
||||
private final Settings settings;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final Dialogs dialogs;
|
||||
private final ObservableValue<VaultPathStatus> vaultPathStatus;
|
||||
private final ObservableValue<Boolean> validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
@@ -80,6 +79,8 @@ public class ImportTemplateLocationController implements FxController {
|
||||
private final ObservableList<Node> radioButtons;
|
||||
private final ObservableList<Node> sortedRadioButtons;
|
||||
|
||||
private final String hubUrl;
|
||||
|
||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||
|
||||
//FXML
|
||||
@@ -94,7 +95,7 @@ public class ImportTemplateLocationController implements FxController {
|
||||
@Inject
|
||||
ImportTemplateLocationController(@ImportTemplateWindow Stage window, //
|
||||
@Named("vaultName") String vaultName, //
|
||||
@Named("vaultTemplate") byte[] template, //
|
||||
VaultTemplate template, //
|
||||
ObjectProperty<Path> vaultPath, //
|
||||
@ImportTemplateWindow ObjectProperty<Vault> vault, //
|
||||
@FxmlScene(FxmlFile.IMPORT_TEMPLATE_SUCCESS) Lazy<Scene> successScene, //
|
||||
@@ -102,8 +103,7 @@ public class ImportTemplateLocationController implements FxController {
|
||||
VaultListManager vaultListManager, //
|
||||
ExecutorService executor, //
|
||||
Settings settings, //
|
||||
ResourceBundle resourceBundle, //
|
||||
Dialogs dialogs) {
|
||||
ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vaultName = vaultName;
|
||||
this.template = template;
|
||||
@@ -115,7 +115,7 @@ public class ImportTemplateLocationController implements FxController {
|
||||
this.executor = executor;
|
||||
this.settings = settings;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.dialogs = dialogs;
|
||||
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);
|
||||
@@ -183,7 +183,10 @@ public class ImportTemplateLocationController implements FxController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
var task = executor.submit(this::loadLocationPresets);
|
||||
window.addEventHandler(WindowEvent.WINDOW_HIDING, _ -> task.cancel(true));
|
||||
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);
|
||||
@@ -248,19 +251,15 @@ public class ImportTemplateLocationController implements FxController {
|
||||
}
|
||||
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(() -> {
|
||||
VaultTemplateExtractor.extractAndMove(template, destination);
|
||||
template.moveTo(destination);
|
||||
return vaultListManager.add(destination);
|
||||
}).onSuccess(newVault -> {
|
||||
vault.set(newVault);
|
||||
rememberParentDirectory(destination);
|
||||
window.setScene(successScene.get());
|
||||
}).onError(VaultTemplateExtractor.MalformedTemplateException.class, e -> { // must precede the IOException handler: Tasks picks the first matching one
|
||||
LOG.error("Vault template is malformed.", e);
|
||||
dialogs.prepareMalformedTemplateDialog(window).setOkAction(stage -> {
|
||||
stage.close();
|
||||
window.close(); //TODO: before showing this dialog, close the window and use the mainWindow as owner
|
||||
}).build().showAndWait();
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to import vault template.", e);
|
||||
appWindows.showErrorWindow(e, window, window.getScene());
|
||||
@@ -286,6 +285,10 @@ public class ImportTemplateLocationController implements FxController {
|
||||
return vaultName;
|
||||
}
|
||||
|
||||
public String getHubUrl() {
|
||||
return hubUrl;
|
||||
}
|
||||
|
||||
public Path getVaultPath() {
|
||||
return vaultPath.get();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A vault template that has been unpacked to a temporary directory and found sound.
|
||||
* <p>
|
||||
* Holding an instance means the template <em>can</em> be imported: it was a readable ZIP within the entry and size
|
||||
* limits, held exactly one decodable {@value Constants#VAULTCONFIG_FILENAME}, and no entry escaped the extraction
|
||||
* directory. Unpacking therefore happens once, when the deeplink arrives, so a template that could never be imported is
|
||||
* rejected before the user is asked to choose a storage location - and {@link #moveTo(Path)} is left with nothing to
|
||||
* validate but the destination.
|
||||
* <p>
|
||||
* The instance owns a 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: its signature is keyed on the masterkey, which is
|
||||
* only obtained by completing the Hub flow. It is shown so the user can recognize an unexpected Hub; the authoritative
|
||||
* trust decision remains with {@code CheckHostTrustController} at unlock time.
|
||||
*/
|
||||
public final class VaultTemplate implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultTemplate.class);
|
||||
|
||||
private final Path tempDir;
|
||||
private final Path vaultRoot;
|
||||
private final @Nullable String hubUrl;
|
||||
|
||||
private VaultTemplate(Path tempDir, Path vaultRoot, @Nullable String hubUrl) {
|
||||
this.tempDir = tempDir;
|
||||
this.vaultRoot = vaultRoot;
|
||||
this.hubUrl = hubUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks and validates the given archive on {@code executor}, off the FX thread.
|
||||
* <p>
|
||||
* Failures arrive as a {@link CompletionException} wrapping the cause, so callers should unwrap before deciding
|
||||
* what to show: 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
|
||||
* @param executor the executor to unpack on
|
||||
* @return the unpacked template, which the caller must close
|
||||
* @see #extract(byte[])
|
||||
*/
|
||||
public static CompletionStage<VaultTemplate> extractAsync(byte[] archive, Executor executor) {
|
||||
return CompletableFuture.supplyAsync(()-> {
|
||||
try {
|
||||
return VaultTemplate.extract(archive);
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 tempDir = tempParent == null //
|
||||
? Files.createTempDirectory("vault-template-") //
|
||||
: Files.createTempDirectory(tempParent, "vault-template-");
|
||||
try {
|
||||
var vaultRoot = VaultTemplateExtractor.extractTo(archive, tempDir);
|
||||
return new VaultTemplate(tempDir, vaultRoot, readHubUrl(vaultRoot));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
VaultTemplateExtractor.deleteQuietly(tempDir);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable String readHubUrl(Path vaultRoot) throws IOException {
|
||||
var token = Files.readString(vaultRoot.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 this template's vault belongs to, 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(vaultRoot, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the temporary directory. Safe to call after a successful {@link #moveTo(Path)}, which only moves the
|
||||
* vault directory out of it.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
VaultTemplateExtractor.deleteQuietly(tempDir);
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,15 @@ import java.util.stream.Stream;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
/**
|
||||
* Unpacks a vault template (a ZIP archive holding a ready-made vault) and moves the contained vault to a destination.
|
||||
* Low-level mechanics of unpacking a vault template (a ZIP archive holding a ready-made vault).
|
||||
* <p>
|
||||
* The archive is expanded into a temporary directory using the Java ZIP {@link FileSystem}, and the vault directory
|
||||
* (identified by containing a {@value Constants#VAULTCONFIG_FILENAME} file) is then moved to the destination in a single
|
||||
* step, so the vault only ever appears complete at its final location.
|
||||
* The archive is expanded into a directory using the Java ZIP {@link FileSystem}, and the vault directory is located by
|
||||
* finding the {@value Constants#VAULTCONFIG_FILENAME} it contains. Moving that directory 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.
|
||||
*/
|
||||
public final class VaultTemplateExtractor {
|
||||
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
|
||||
@@ -39,34 +41,41 @@ public final class VaultTemplateExtractor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks the given template and moves the contained vault to {@code destination}.
|
||||
* Unpacks the given template into {@code targetDir} and locates the vault it contains.
|
||||
*
|
||||
* @param template the ZIP archive bytes
|
||||
* @param destination the target vault directory, which must not yet exist
|
||||
* @return {@code destination}
|
||||
* @throws MalformedTemplateException if the template is not a valid zip or does not contain a vault
|
||||
* @throws IOException if the destination already exists, or the
|
||||
* files cannot be written or moved
|
||||
* @param template the ZIP archive bytes
|
||||
* @param targetDir an existing, empty directory to unpack into
|
||||
* @return the unpacked vault directory below {@code targetDir}
|
||||
* @throws MalformedTemplateException if the template is not a readable ZIP, exceeds the entry or size limits, or
|
||||
* does not contain exactly one {@value Constants#VAULTCONFIG_FILENAME}
|
||||
* @throws IOException if the files cannot be written
|
||||
*/
|
||||
public static Path extractAndMove(byte[] template, Path destination) throws IOException {
|
||||
static Path extractTo(byte[] template, Path targetDir) throws IOException {
|
||||
Path tmpZip = Files.createTempFile("vault-template-", ".zip");
|
||||
try {
|
||||
Files.write(tmpZip, template);
|
||||
return unzip(tmpZip, targetDir);
|
||||
} finally {
|
||||
deleteQuietly(tmpZip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 tmpZip = Files.createTempFile("vault-template-", ".zip");
|
||||
Path tmpDir = Files.createTempDirectory("vault-template-");
|
||||
try {
|
||||
Files.write(tmpZip, template);
|
||||
Path vaultRoot = unzip(tmpZip, tmpDir);
|
||||
Path parent = destination.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
move(vaultRoot, destination);
|
||||
return destination;
|
||||
} finally {
|
||||
deleteQuietly(tmpDir);
|
||||
deleteQuietly(tmpZip);
|
||||
Path parent = destination.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
move(vaultRoot, destination);
|
||||
}
|
||||
|
||||
private static Path unzip(Path zipFile, Path targetDir) throws IOException {
|
||||
@@ -183,7 +192,7 @@ public final class VaultTemplateExtractor {
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteQuietly(Path path) {
|
||||
static void deleteQuietly(Path path) {
|
||||
try {
|
||||
deleteRecursively(path);
|
||||
} catch (IOException e) {
|
||||
@@ -203,16 +212,4 @@ public final class VaultTemplateExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
public static 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
<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"/>
|
||||
|
||||
@@ -669,6 +669,8 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user