Show user improved error message

This commit is contained in:
Armin Schrenk
2026-05-12 08:12:22 +02:00
parent 6c925d6a1f
commit e63a0e8ee8
6 changed files with 93 additions and 43 deletions

View File

@@ -0,0 +1,32 @@
package org.cryptomator.common.vaults;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
public class NotAVaultDirectoryException extends NoSuchFileException {
public enum Reason {
MISSING_DATA_DIR,
DATA_NOT_A_DIRECTORY,
MISSING_VAULT_CONFIG,
VAULT_CONFIG_ACCESS_DENIED,
UNSUPPORTED_STRUCTURE
}
private final transient Path path;
private final Reason reason;
public NotAVaultDirectoryException(Path path, Reason reason) {
super(path.toString(), null, "Not a vault directory: " + reason);
this.path = path;
this.reason = reason;
}
public Path path() {
return path;
}
public Reason notAVaultReason() {
return reason;
}
}

View File

@@ -25,11 +25,9 @@ import javax.inject.Singleton;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -85,40 +83,28 @@ public class VaultListManager {
});
}
static void assertIsVaultDirectory(Path pathToVault) throws IOException {
public static void assertIsVaultDirectory(Path pathToVault) throws IOException {
if (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
throw new NoSuchFileException(pathToVault.toString(), null, "Not a vault directory: " + determineNotVaultDirectoryReason(pathToVault));
}
}
Path dataDir = pathToVault.resolve(DATA_DIR_NAME);
if (!Files.isDirectory(dataDir)) {
if (Files.exists(dataDir)) {
throw new NotAVaultDirectoryException(pathToVault, NotAVaultDirectoryException.Reason.DATA_NOT_A_DIRECTORY);
} else {
throw new NotAVaultDirectoryException(pathToVault, NotAVaultDirectoryException.Reason.MISSING_DATA_DIR);
}
}
private static String determineNotVaultDirectoryReason(Path pathToVault) {
Path dataDir = pathToVault.resolve(DATA_DIR_NAME);
if (!Files.isDirectory(dataDir)) {
return describeNotDirectory(dataDir);
}
Path vaultConfig = pathToVault.resolve(VAULTCONFIG_FILENAME);
if (!Files.isReadable(vaultConfig)) {
if (Files.exists(vaultConfig)) {
throw new NotAVaultDirectoryException(pathToVault, NotAVaultDirectoryException.Reason.VAULT_CONFIG_ACCESS_DENIED);
} else {
throw new NotAVaultDirectoryException(pathToVault, NotAVaultDirectoryException.Reason.MISSING_VAULT_CONFIG);
}
}
Path vaultConfig = pathToVault.resolve(VAULTCONFIG_FILENAME);
if (!Files.isReadable(vaultConfig)) {
Path masterkey = pathToVault.resolve(MASTERKEY_FILENAME);
return describeNotReadable(vaultConfig) + "; " + describeNotReadable(masterkey) + " for legacy vault detection";
}
return "directory structure is unsupported";
}
private static String describeNotDirectory(Path path) {
if (Files.exists(path)) {
return path.getFileName() + " is not a directory";
} else {
return path.getFileName() + " directory is missing";
}
}
private static String describeNotReadable(Path path) {
if (Files.exists(path)) {
return path.getFileName() + " is not readable";
} else {
return path.getFileName() + " is missing";
//if vault is legacy _and_ not readable, just say unsupported
throw new NotAVaultDirectoryException(pathToVault, NotAVaultDirectoryException.Reason.UNSUPPORTED_STRUCTURE);
}
}
@@ -189,7 +175,7 @@ public class VaultListManager {
//for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state
vaultSettings.lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME);
}
case VAULT_CONFIG_MISSING -> {
case VAULT_CONFIG_MISSING -> {
//Nothing to do here, since there is no config to read
}
case MISSING, ALL_MISSING, ERROR, PROCESSING -> {

View File

@@ -2,12 +2,14 @@ package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.NotAVaultDirectoryException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.fxapp.FxApplicationStyle;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
@@ -41,6 +43,7 @@ public class ChooseExistingVaultController implements FxController {
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final Dialogs dialogs;
private final ObservableValue<Image> screenshot;
@Inject
@@ -51,6 +54,7 @@ public class ChooseExistingVaultController implements FxController {
@AddVaultWizardWindow ObjectProperty<Vault> vault, //
VaultListManager vaultListManager, //
ResourceBundle resourceBundle, //
Dialogs dialogs, //
FxApplicationStyle applicationStyle) {
this.window = window;
this.successScene = successScene;
@@ -59,6 +63,7 @@ public class ChooseExistingVaultController implements FxController {
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.dialogs = dialogs;
this.screenshot = applicationStyle.appliedAppThemeProperty().map(this::selectScreenshot);
}
@@ -87,6 +92,9 @@ public class ChooseExistingVaultController implements FxController {
Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault);
window.setScene(successScene.get());
} catch (NotAVaultDirectoryException e) {
LOG.warn("Selected folder is not a vault directory: {}", e.getMessage());
dialogs.prepareNotAVaultDirectoryDialog(window, e.notAVaultReason()).build().showAndWait();
} catch (IOException e) {
LOG.error("Failed to open existing vault.", e);
appWindows.showErrorWindow(e, window, window.getScene());

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.dialogs;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.NotAVaultDirectoryException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.StageFactory;
@@ -139,6 +140,24 @@ public class Dialogs {
.setCancelAction(Stage::close);
}
public SimpleDialog.Builder prepareNotAVaultDirectoryDialog(Stage window, NotAVaultDirectoryException.Reason reason) {
String descriptionKey = switch (reason) {
case MISSING_DATA_DIR -> "addvaultwizard.existing.notAVault.description.missingDataDir";
case DATA_NOT_A_DIRECTORY -> "addvaultwizard.existing.notAVault.description.dataNotADirectory";
case MISSING_VAULT_CONFIG -> "addvaultwizard.existing.notAVault.description.missingVaultConfig";
case VAULT_CONFIG_ACCESS_DENIED -> "addvaultwizard.existing.notAVault.description.vaultConfigAccessDenied";
case UNSUPPORTED_STRUCTURE -> "addvaultwizard.existing.notAVault.description.unsupportedStructure";
};
return createDialogBuilder() //
.setOwner(window) //
.setTitleKey("addvaultwizard.existing.notAVault.title") //
.setMessageKey("addvaultwizard.existing.notAVault.message") //
.setDescriptionKey(descriptionKey) //
.setIcon(FontAwesome5Icon.EXCLAMATION) //
.setOkButtonKey(BUTTON_KEY_CLOSE) //
.setOkAction(Stage::close);
}
public SimpleDialog.Builder prepareNoDDirectorySelectedDialog(Stage window) {
return createDialogBuilder() //
.setOwner(window) //

View File

@@ -110,6 +110,13 @@ addvaultwizard.existing.restore=Restore…
addvaultwizard.existing.chooseBtn=Choose…
addvaultwizard.existing.filePickerTitle=Select Vault File
addvaultwizard.existing.filePickerMimeDesc=Cryptomator Vault
addvaultwizard.existing.notAVault.title=Not a Vault
addvaultwizard.existing.notAVault.message=The selected folder is not a Cryptomator vault
addvaultwizard.existing.notAVault.description.missingDataDir=The required "d" subdirectory is missing inside the selected folder.
addvaultwizard.existing.notAVault.description.dataNotADirectory=The "d" entry inside the selected folder is not a directory.
addvaultwizard.existing.notAVault.description.missingVaultConfig=The required "vault.cryptomator" file is missing inside the selected folder .
addvaultwizard.existing.notAVault.description.vaultConfigAccessDenied=File "vault.cryptomator" cannot be read due to insufficient access rights.
addvaultwizard.existing.notAVault.description.unsupportedStructure=The directory structure of the selected folder is not supported.
## Success
addvaultwizard.success.nextStepsInstructions=Added vault "%s".\nYou need to unlock this vault to access or add contents. Alternatively you can unlock it at any later point in time.
addvaultwizard.success.unlockNow=Unlock Now

View File

@@ -5,48 +5,46 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VaultListManagerTest {
@Test
void testAssertIsVaultDirectoryWhenDataDirIsMissing(@TempDir Path tmpDir) {
NoSuchFileException e = assertThrows(NoSuchFileException.class, () -> {
NotAVaultDirectoryException e = assertThrows(NotAVaultDirectoryException.class, () -> {
VaultListManager.assertIsVaultDirectory(tmpDir);
});
assertTrue(e.getReason().contains(DATA_DIR_NAME + " directory is missing"));
assertEquals(NotAVaultDirectoryException.Reason.MISSING_DATA_DIR, e.notAVaultReason());
}
@Test
void testAssertIsVaultDirectoryWhenDataDirIsFile(@TempDir Path tmpDir) throws IOException {
Files.createFile(tmpDir.resolve(DATA_DIR_NAME));
NoSuchFileException e = assertThrows(NoSuchFileException.class, () -> {
NotAVaultDirectoryException e = assertThrows(NotAVaultDirectoryException.class, () -> {
VaultListManager.assertIsVaultDirectory(tmpDir);
});
assertTrue(e.getReason().contains(DATA_DIR_NAME + " is not a directory"));
assertEquals(NotAVaultDirectoryException.Reason.DATA_NOT_A_DIRECTORY, e.notAVaultReason());
}
@Test
void testAssertIsVaultDirectoryWhenVaultConfigAndMasterkeyAreMissing(@TempDir Path tmpDir) throws IOException {
Files.createDirectory(tmpDir.resolve(DATA_DIR_NAME));
NoSuchFileException e = assertThrows(NoSuchFileException.class, () -> {
NotAVaultDirectoryException e = assertThrows(NotAVaultDirectoryException.class, () -> {
VaultListManager.assertIsVaultDirectory(tmpDir);
});
assertTrue(e.getReason().contains(VAULTCONFIG_FILENAME + " is missing"));
assertTrue(e.getReason().contains(MASTERKEY_FILENAME + " is missing"));
assertEquals(NotAVaultDirectoryException.Reason.MISSING_VAULT_CONFIG, e.notAVaultReason());
}
@Test