|
@@ -6,6 +6,7 @@ import com.google.common.cache.CacheLoader;
|
|
|
import com.google.common.cache.LoadingCache;
|
|
|
import com.tobiasdiez.easybind.EasyBind;
|
|
|
import org.apache.commons.lang3.SystemUtils;
|
|
|
+import org.cryptomator.common.Nullable;
|
|
|
import org.cryptomator.common.vaults.Vault;
|
|
|
import org.cryptomator.integrations.mount.Mountpoint;
|
|
|
import org.cryptomator.integrations.revealpath.RevealFailedException;
|
|
@@ -39,10 +40,14 @@ import java.io.IOException;
|
|
|
import java.nio.file.Path;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
import java.util.Optional;
|
|
|
import java.util.ResourceBundle;
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.function.Consumer;
|
|
|
+import java.util.function.Function;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
@MainWindowScoped
|
|
|
public class VaultDetailUnlockedController implements FxController {
|
|
@@ -62,11 +67,15 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
private final ObservableValue<Boolean> accessibleViaPath;
|
|
|
private final ObservableValue<Boolean> accessibleViaUri;
|
|
|
private final ObservableValue<String> mountPoint;
|
|
|
- private final BooleanProperty draggingOver = new SimpleBooleanProperty();
|
|
|
+ private final BooleanProperty draggingOverLocateEncrypted = new SimpleBooleanProperty();
|
|
|
+ private final BooleanProperty draggingOverDecryptName = new SimpleBooleanProperty();
|
|
|
private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
|
|
|
+ private final BooleanProperty cleartextNamesCopied = new SimpleBooleanProperty();
|
|
|
|
|
|
- //FXML
|
|
|
- public Button dropZone;
|
|
|
+ @FXML
|
|
|
+ public Button revealEncryptedDropZone;
|
|
|
+ @FXML
|
|
|
+ public Button decryptNameDropZone;
|
|
|
|
|
|
@Inject
|
|
|
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, Optional<RevealPathService> revealPathService, ResourceBundle resourceBundle) {
|
|
@@ -92,72 +101,90 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
}
|
|
|
|
|
|
public void initialize() {
|
|
|
- dropZone.setOnDragEntered(this::handleDragEvent);
|
|
|
- dropZone.setOnDragOver(this::handleDragEvent);
|
|
|
- dropZone.setOnDragDropped(this::handleDragEvent);
|
|
|
- dropZone.setOnDragExited(this::handleDragEvent);
|
|
|
+ revealEncryptedDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverLocateEncrypted));
|
|
|
+ revealEncryptedDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCiphertextPath, this::revealOrCopyPaths));
|
|
|
+ revealEncryptedDropZone.setOnDragExited(_ -> draggingOverLocateEncrypted.setValue(false));
|
|
|
+
|
|
|
+ decryptNameDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverDecryptName));
|
|
|
+ decryptNameDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCleartextName, this::copyDecryptedNamesToClipboard));
|
|
|
+ decryptNameDropZone.setOnDragExited(_ -> draggingOverDecryptName.setValue(false));
|
|
|
|
|
|
- EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver);
|
|
|
+ EasyBind.includeWhen(revealEncryptedDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverLocateEncrypted);
|
|
|
+ EasyBind.includeWhen(decryptNameDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverDecryptName);
|
|
|
}
|
|
|
|
|
|
- private void handleDragEvent(DragEvent event) {
|
|
|
- if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
|
|
|
- if(SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
|
|
|
+ private void handleDragOver(DragEvent event, BooleanProperty prop) {
|
|
|
+ if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
|
|
|
+ if (SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
|
|
|
event.acceptTransferModes(TransferMode.LINK);
|
|
|
} else {
|
|
|
event.acceptTransferModes(TransferMode.ANY);
|
|
|
}
|
|
|
- draggingOver.set(true);
|
|
|
- } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
|
|
|
- List<Path> ciphertextPaths = event.getDragboard().getFiles().stream().map(File::toPath).map(this::getCiphertextPath).flatMap(Optional::stream).toList();
|
|
|
- if (ciphertextPaths.isEmpty()) {
|
|
|
+ prop.set(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private <T> void handleDragDropped(DragEvent event, Function<Path, T> computation, Consumer<List<T>> positiveAction) {
|
|
|
+ if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
|
|
|
+ List<T> objects = event.getDragboard().getFiles().stream().map(File::toPath).map(computation).filter(Objects::nonNull).toList();
|
|
|
+ if (objects.isEmpty()) {
|
|
|
wrongFileAlert.build().showWrongFileAlertWindow();
|
|
|
} else {
|
|
|
- revealOrCopyPaths(ciphertextPaths);
|
|
|
+ positiveAction.accept(objects);
|
|
|
}
|
|
|
- event.setDropCompleted(!ciphertextPaths.isEmpty());
|
|
|
+ event.setDropCompleted(!objects.isEmpty());
|
|
|
event.consume();
|
|
|
- } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
|
|
|
- draggingOver.set(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private VaultStatisticsComponent buildVaultStats(Vault vault) {
|
|
|
- return vaultStatsBuilder.vault(vault).build();
|
|
|
- }
|
|
|
-
|
|
|
@FXML
|
|
|
- public void revealAccessLocation() {
|
|
|
- vaultService.reveal(vault.get());
|
|
|
+ public void chooseDecryptedFileAndReveal() {
|
|
|
+ Preconditions.checkState(accessibleViaPath.getValue());
|
|
|
+ var fileChooser = new FileChooser();
|
|
|
+ fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.locateEncrypted.filePickerTitle"));
|
|
|
+ fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
|
|
|
+ var cleartextFile = fileChooser.showOpenDialog(mainWindow);
|
|
|
+ if (cleartextFile != null) {
|
|
|
+ var ciphertextPath = getCiphertextPath(cleartextFile.toPath());
|
|
|
+ if (ciphertextPath != null) {
|
|
|
+ revealOrCopyPaths(List.of(ciphertextPath));
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@FXML
|
|
|
- public void copyMountUri() {
|
|
|
- ClipboardContent clipboardContent = new ClipboardContent();
|
|
|
- clipboardContent.putString(mountPoint.getValue());
|
|
|
- Clipboard.getSystemClipboard().setContent(clipboardContent);
|
|
|
- }
|
|
|
+ public void chooseEncryptedFileAndCopyNames() {
|
|
|
+ var fileChooser = new FileChooser();
|
|
|
+ fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.decryptName.filePickerTitle"));
|
|
|
|
|
|
- @FXML
|
|
|
- public void lock() {
|
|
|
- appWindows.startLockWorkflow(vault.get(), mainWindow);
|
|
|
+ fileChooser.setInitialDirectory(vault.getValue().getPath().toFile());
|
|
|
+ var ciphertextNode = fileChooser.showOpenDialog(mainWindow);
|
|
|
+ if (ciphertextNode != null) {
|
|
|
+ var nodeName = getCleartextName(ciphertextNode.toPath());
|
|
|
+ copyDecryptedNamesToClipboard(List.of(nodeName));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- @FXML
|
|
|
- public void showVaultStatistics() {
|
|
|
- vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
|
|
|
+ private void copyDecryptedNamesToClipboard(List<CipherToCleartext> mapping) {
|
|
|
+ if (mapping.size() == 1) {
|
|
|
+ Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, mapping.getFirst().cleartext));
|
|
|
+ } else {
|
|
|
+ var content = mapping.stream().map(CipherToCleartext::toString).collect(Collectors.joining("\n"));
|
|
|
+ Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, content));
|
|
|
+ }
|
|
|
+ cleartextNamesCopied.setValue(true);
|
|
|
+ CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
|
|
|
+ cleartextNamesCopied.set(false);
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- @FXML
|
|
|
- public void chooseFileAndReveal() {
|
|
|
- Preconditions.checkState(accessibleViaPath.getValue());
|
|
|
- var fileChooser = new FileChooser();
|
|
|
- fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle"));
|
|
|
- fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
|
|
|
- var cleartextFile = fileChooser.showOpenDialog(mainWindow);
|
|
|
- if (cleartextFile != null) {
|
|
|
- var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList();
|
|
|
- revealOrCopyPaths(ciphertextPaths);
|
|
|
+ @Nullable
|
|
|
+ private CipherToCleartext getCleartextName(Path ciphertextNode) {
|
|
|
+ try {
|
|
|
+ return new CipherToCleartext(ciphertextNode.getFileName().toString(), vault.get().getCleartextName(ciphertextNode));
|
|
|
+ } catch (IOException e) {
|
|
|
+ LOG.warn("Failed to decrypt filename for {}", ciphertextNode, e);
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -165,16 +192,17 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
return path.startsWith(Path.of(mountPoint.getValue()));
|
|
|
}
|
|
|
|
|
|
- private Optional<Path> getCiphertextPath(Path path) {
|
|
|
+ @Nullable
|
|
|
+ private Path getCiphertextPath(Path path) {
|
|
|
if (!startsWithVaultAccessPoint(path)) {
|
|
|
- LOG.debug("Path does not start with access point of selected vault: {}", path);
|
|
|
- return Optional.empty();
|
|
|
+ LOG.debug("Path does not start with mount point of selected vault: {}", path);
|
|
|
+ return null;
|
|
|
}
|
|
|
try {
|
|
|
- return Optional.of(vault.get().getCiphertextPath(path));
|
|
|
+ return vault.get().getCiphertextPath(path);
|
|
|
} catch (IOException e) {
|
|
|
LOG.warn("Unable to get ciphertext path from path: {}", path, e);
|
|
|
- return Optional.empty();
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -206,6 +234,40 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ private VaultStatisticsComponent buildVaultStats(Vault vault) {
|
|
|
+ return vaultStatsBuilder.vault(vault).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ public void revealAccessLocation() {
|
|
|
+ vaultService.reveal(vault.get());
|
|
|
+ }
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ public void copyMountUri() {
|
|
|
+ ClipboardContent clipboardContent = new ClipboardContent();
|
|
|
+ clipboardContent.putString(mountPoint.getValue());
|
|
|
+ Clipboard.getSystemClipboard().setContent(clipboardContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ public void lock() {
|
|
|
+ appWindows.startLockWorkflow(vault.get(), mainWindow);
|
|
|
+ }
|
|
|
+
|
|
|
+ @FXML
|
|
|
+ public void showVaultStatistics() {
|
|
|
+ vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
|
|
|
+ }
|
|
|
+
|
|
|
+ record CipherToCleartext(String ciphertext, String cleartext) {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return ciphertext + " > " + cleartext;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/* Getter/Setter */
|
|
|
|
|
|
public ReadOnlyObjectProperty<Vault> vaultProperty() {
|
|
@@ -247,4 +309,12 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
public boolean isCiphertextPathsCopied() {
|
|
|
return ciphertextPathsCopied.get();
|
|
|
}
|
|
|
+
|
|
|
+ public BooleanProperty cleartextNamesCopiedProperty() {
|
|
|
+ return cleartextNamesCopied;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isCleartextNamesCopied() {
|
|
|
+ return cleartextNamesCopied.get();
|
|
|
+ }
|
|
|
}
|