mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-25 00:26:03 +00:00
Improve error handling
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
@@ -16,8 +16,8 @@ import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
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.
|
||||
@@ -41,7 +41,8 @@ public final class VaultTemplateExtractor {
|
||||
* @param template the ZIP archive bytes
|
||||
* @param destination the target vault directory, which must not yet exist
|
||||
* @return {@code destination}
|
||||
* @throws IOException if the template is not a valid ZIP, contains no vault, the destination already exists, or the
|
||||
* @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
|
||||
*/
|
||||
public static Path extractAndMove(byte[] template, Path destination) throws IOException {
|
||||
@@ -66,58 +67,81 @@ public final class VaultTemplateExtractor {
|
||||
}
|
||||
|
||||
private static Path unzip(Path zipFile, Path targetDir) throws IOException {
|
||||
AtomicReference<Path> vaultConfig = new AtomicReference<>();
|
||||
long[] totalBytes = {0};
|
||||
int[] entryCount = {0};
|
||||
Path normalizedTarget = targetDir.normalize();
|
||||
try (FileSystem zipFs = FileSystems.newFileSystem(zipFile)) {
|
||||
for (Path zipRoot : zipFs.getRootDirectories()) {
|
||||
Files.walkFileTree(zipRoot, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (!dir.equals(zipRoot)) {
|
||||
countEntry(entryCount);
|
||||
}
|
||||
Files.createDirectories(resolveSafely(normalizedTarget, zipRoot, dir));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
Path zipRoot = zipFs.getRootDirectories().iterator().next(); //we take the first available root and ignore others
|
||||
var templateExtractor = new TemplateExtractionVisitor(zipRoot, normalizedTarget, MAX_ENTRIES, MAX_TOTAL_BYTES);
|
||||
Files.walkFileTree(zipRoot, templateExtractor);
|
||||
var vaultConfig = templateExtractor.getVaultConfig();
|
||||
if (vaultConfig == null) {
|
||||
throw new MalformedTemplateException("Template does not contain a vault (no " + Constants.VAULTCONFIG_FILENAME + " found).");
|
||||
}
|
||||
return vaultConfig.getParent();
|
||||
} catch (ZipException e) {
|
||||
throw new MalformedTemplateException("Template is not a readable ZIP archive.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
countEntry(entryCount);
|
||||
totalBytes[0] += attrs.size();
|
||||
if (totalBytes[0] > MAX_TOTAL_BYTES) {
|
||||
throw new IOException("Vault template exceeds the maximum allowed size of " + MAX_TOTAL_BYTES + " bytes.");
|
||||
}
|
||||
var target = resolveSafely(normalizedTarget, zipRoot, file);
|
||||
Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
if (file.getFileName() != null && Constants.VAULTCONFIG_FILENAME.equals(file.getFileName().toString())) {
|
||||
if (!vaultConfig.compareAndSet(null, target)) {
|
||||
throw new IOException("Vault template contains more than one " + Constants.VAULTCONFIG_FILENAME);
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
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;
|
||||
private Path vaultConfig = null;
|
||||
|
||||
TemplateExtractionVisitor(Path zipRoot, Path target, int maxEntries, long maxSize) {
|
||||
this.zipRoot = zipRoot;
|
||||
this.target = target;
|
||||
this.maxEntries = maxEntries + 1; //counting the root directory
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
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.");
|
||||
}
|
||||
|
||||
var actualTarget = Files.copy(file, resolveSafely(target, zipRoot, file), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
if (Constants.VAULTCONFIG_FILENAME.equals(file.getFileName().toString())) {
|
||||
if (vaultConfig != null) {
|
||||
throw new MalformedTemplateException("Vault template contains more than one " + Constants.VAULTCONFIG_FILENAME);
|
||||
}
|
||||
vaultConfig = actualTarget;
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
if (vaultConfig.get() == null) {
|
||||
throw new IOException("Template does not contain a vault (no " + Constants.VAULTCONFIG_FILENAME + " found).");
|
||||
}
|
||||
return vaultConfig.get().getParent();
|
||||
}
|
||||
|
||||
private static void countEntry(int[] entryCount) throws IOException {
|
||||
if (++entryCount[0] > MAX_ENTRIES) {
|
||||
throw new IOException("Vault template contains more than the maximum allowed " + MAX_ENTRIES + " entries.");
|
||||
Path getVaultConfig() {
|
||||
return vaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
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 IOException("Refusing to extract entry outside of target directory: " + entry);
|
||||
throw new MalformedTemplateException("Refusing to extract entry outside of target directory: " + entry);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
@@ -174,4 +198,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -158,6 +158,17 @@ 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
-1
@@ -8,11 +8,13 @@ import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.launcher.VaultTemplateExtractor;
|
||||
import org.cryptomator.launcher.VaultTemplateExtractor.MalformedTemplateException;
|
||||
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.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -69,6 +71,7 @@ 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;
|
||||
@@ -101,7 +104,8 @@ public class ImportTemplateLocationController implements FxController {
|
||||
VaultListManager vaultListManager, //
|
||||
ExecutorService executor, //
|
||||
Settings settings, //
|
||||
ResourceBundle resourceBundle) {
|
||||
ResourceBundle resourceBundle, //
|
||||
Dialogs dialogs) {
|
||||
this.window = window;
|
||||
this.vaultName = vaultName;
|
||||
this.template = template;
|
||||
@@ -113,6 +117,7 @@ public class ImportTemplateLocationController implements FxController {
|
||||
this.executor = executor;
|
||||
this.settings = settings;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.dialogs = dialogs;
|
||||
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);
|
||||
@@ -252,6 +257,12 @@ public class ImportTemplateLocationController implements FxController {
|
||||
vault.set(newVault);
|
||||
rememberParentDirectory(destination);
|
||||
window.setScene(successScene.get());
|
||||
}).onError(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());
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<children>
|
||||
<VBox spacing="6">
|
||||
<Label text="%importTemplate.nameLabel"/>
|
||||
<TextField text="${controller.vaultName}" editable="false" disable="true"/>
|
||||
</VBox>
|
||||
<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"/>
|
||||
|
||||
|
||||
@@ -670,7 +670,9 @@ retryIfReadonly.retry=Change and Retry
|
||||
# Import Template
|
||||
importTemplate.title=Setup Vault
|
||||
importTemplate.nameLabel=Vault name
|
||||
importTemplate.createVaultBtn=Create Vault
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.cryptomator.launcher.VaultTemplateExtractor.MalformedTemplateException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -63,26 +64,34 @@ public class VaultTemplateExtractorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive without a vault config fails")
|
||||
public void testNoVaultConfig(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of("readme.txt", CONFIG));
|
||||
@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(IOException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractAndMove(notAZip, tmp.resolve("MyVault")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive with more than one vault config fails")
|
||||
@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.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive with more than one vault config fails as malformed")
|
||||
public void testMultipleVaultConfigs(@TempDir Path tmp) throws IOException {
|
||||
var zip = zip(Map.of( //
|
||||
"vault.cryptomator", CONFIG, //
|
||||
"nested/vault.cryptomator", CONFIG //
|
||||
));
|
||||
|
||||
Assertions.assertThrows(IOException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive exceeding the size limit fails")
|
||||
@DisplayName("an archive exceeding the size limit fails as malformed")
|
||||
public void testExceedsSizeLimit(@TempDir Path tmp) throws IOException {
|
||||
var big = new byte[2 * 1024 * 1024 + 1];
|
||||
var zip = zip(Map.of( //
|
||||
@@ -90,20 +99,20 @@ public class VaultTemplateExtractorTest {
|
||||
"d/AB/CDEF/0.c9r", big //
|
||||
));
|
||||
|
||||
Assertions.assertThrows(IOException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an archive exceeding the entry-count limit fails")
|
||||
@DisplayName("an archive exceeding the entry-count limit fails as malformed")
|
||||
public void testExceedsEntryLimit(@TempDir Path tmp) throws IOException {
|
||||
var entries = new LinkedHashMap<String, byte[]>();
|
||||
entries.put("vault.cryptomator", CONFIG);
|
||||
for (int i = 0; i < 11; i++) {
|
||||
for (int i = 0; i < 31; i++) {
|
||||
entries.put("file" + i + ".c9r", CIPHERTEXT);
|
||||
}
|
||||
var zip = zip(entries);
|
||||
|
||||
Assertions.assertThrows(IOException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
Assertions.assertThrows(MalformedTemplateException.class, () -> VaultTemplateExtractor.extractAndMove(zip, tmp.resolve("MyVault")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user