Added drop zone for revealing encrypted files or folders (#2592)

* Added drop zone for revealing encrypted files or folders

* Split god drop zone into distinct drop zones in vault list and vault detail unlocked

* Prevent cursor from changing if content doesn't match during drag over

[ci skip]

* Removed unused methods / css classes

[ci skip]

* Updated method name

* Refactor Vault::getCiphertextPath to only accept strings starting with "/"

* bump cryptofs

* ensure that path cleartext path always starts with "/"

* added file chooser for revealing encrypted items

* Trying to fix path parsing again

* Updated drag & drop design for buttons

* use RevealPathService to reveal files in file manager

* fix compilation error

* Copy paths to clipboard if no revealService is present

* reintegrate wongFileAlert

* Only accept TrasnferMode.LINK

* updated drag-n-drop button styling

* added tooltip

* updated string

* cleanup

Co-authored-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
Tobias Hagemann
2023-01-20 10:46:00 +01:00
committed by GitHub
parent b8326907bf
commit d6388d6205
12 changed files with 367 additions and 163 deletions

View File

@@ -339,6 +339,23 @@ public class Vault {
return vaultSettings.path().getValue();
}
/**
* Gets from the cleartext path its ciphertext counterpart.
* The cleartext path has to start from the vault root (by starting with "/").
*
* @return Local os path to the ciphertext resource
* @throws IOException if an I/O error occurs
*/
public Path getCiphertextPath(String cleartextPath) throws IOException {
if (!cleartextPath.startsWith("/")) {
throw new IllegalArgumentException("Input path must be absolute from vault root by starting with \"/\".");
}
var fs = cryptoFileSystem.get();
var cryptoPath = fs.getPath(cleartextPath);
return fs.getCiphertextPath(cryptoPath);
}
public boolean isHavingCustomMountFlags() {
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
}

View File

@@ -25,6 +25,7 @@ public enum FontAwesome5Icon {
EYE_SLASH("\uF070"), //
FAST_FORWARD("\uF050"), //
FILE("\uF15B"), //
FILE_DOWNLOAD("\uF56D"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
FUNNEL("\uF0B0"), //

View File

@@ -3,33 +3,17 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MainWindowScoped
public class MainWindowController implements FxController {
@@ -37,28 +21,19 @@ public class MainWindowController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private final Stage window;
private final VaultListManager vaultListManager;
private final ReadOnlyObjectProperty<Vault> selectedVault;
private final WrongFileAlertComponent.Builder wrongFileAlert;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public StackPane root;
@Inject
public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty<Vault> selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
public MainWindowController(@MainWindow Stage window, ObjectProperty<Vault> selectedVault) {
this.window = window;
this.vaultListManager = vaultListManager;
this.selectedVault = selectedVault;
this.wrongFileAlert = wrongFileAlert;
}
@FXML
public void initialize() {
LOG.trace("init MainWindowController");
root.setOnDragEntered(this::handleDragEvent);
root.setOnDragOver(this::handleDragEvent);
root.setOnDragDropped(this::handleDragEvent);
root.setOnDragExited(this::handleDragEvent);
if (SystemUtils.IS_OS_WINDOWS) {
root.getStyleClass().add("os-windows");
}
@@ -72,65 +47,4 @@ public class MainWindowController implements FxController {
}
}
private void handleDragEvent(DragEvent event) {
if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) {
draggingOver.set(true);
} else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.ANY);
draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
} else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Set<Path> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
if (vaultPaths.isEmpty()) {
wrongFileAlert.build().showWrongFileAlertWindow();
} else {
vaultPaths.forEach(this::addVault);
}
event.setDropCompleted(!vaultPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingOver.set(false);
draggingVaultOver.set(false);
}
}
private boolean containsVault(Path path) {
try {
if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
path = path.getParent();
}
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
} catch (IOException e) {
return false;
}
}
private void addVault(Path pathToVault) {
try {
if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
vaultListManager.add(pathToVault.getParent());
} else {
vaultListManager.add(pathToVault);
}
} catch (IOException e) {
LOG.debug("Not a vault: {}", pathToVault);
}
}
/* Getter/Setter */
public BooleanProperty draggingOverProperty() {
return draggingOver;
}
public boolean isDraggingOver() {
return draggingOver.get();
}
public BooleanProperty draggingVaultOverProperty() {
return draggingVaultOver;
}
public boolean isDraggingVaultOver() {
return draggingVaultOver.get();
}
}

View File

@@ -3,38 +3,99 @@ package org.cryptomator.ui.mainwindow;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.revealpath.RevealFailedException;
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class);
private static final String ACTIVE_CLASS = "active";
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
private final WrongFileAlertComponent.Builder wrongFileAlert;
private final Stage mainWindow;
private final ResourceBundle resourceBundle;
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
public Button dropZone;
@Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
this.vault = vault;
this.appWindows = appWindows;
this.vaultService = vaultService;
this.wrongFileAlert = wrongFileAlert;
this.mainWindow = mainWindow;
this.resourceBundle = resourceBundle;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
this.vaultStatsBuilder = vaultStatsBuilder;
}
public void initialize() {
dropZone.setOnDragEntered(this::handleDragEvent);
dropZone.setOnDragOver(this::handleDragEvent);
dropZone.setOnDragDropped(this::handleDragEvent);
dropZone.setOnDragExited(this::handleDragEvent);
EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver);
}
private void handleDragEvent(DragEvent event) {
if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.LINK);
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()) {
wrongFileAlert.build().showWrongFileAlertWindow();
} else {
revealOrCopyPaths(ciphertextPaths);
}
event.setDropCompleted(!ciphertextPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingOver.set(false);
}
}
private VaultStatisticsComponent buildVaultStats(Vault vault) {
return vaultStatsBuilder.vault(vault).build();
}
@@ -54,6 +115,76 @@ public class VaultDetailUnlockedController implements FxController {
vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
}
@FXML
public void chooseFileAndReveal() {
var fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle"));
fileChooser.setInitialDirectory(Path.of(vault.get().getAccessPoint()).toFile());
var cleartextFile = fileChooser.showOpenDialog(mainWindow);
if (cleartextFile != null) {
var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList();
revealOrCopyPaths(ciphertextPaths);
}
}
private boolean startsWithVaultAccessPoint(Path path) {
return path.startsWith(vault.get().getAccessPoint());
}
private Optional<Path> getCiphertextPath(Path path) {
if (!startsWithVaultAccessPoint(path)) {
LOG.debug("Path does not start with access point of selected vault: {}", path);
return Optional.empty();
}
try {
var accessPoint = vault.get().getAccessPoint();
var cleartextPath = path.toString().substring(accessPoint.length());
if (!cleartextPath.startsWith("/")) {
cleartextPath = "/" + cleartextPath;
}
return Optional.of(vault.get().getCiphertextPath(cleartextPath));
} catch (IOException e) {
LOG.warn("Unable to get ciphertext path from path: {}", path);
return Optional.empty();
}
}
private void revealOrCopyPaths(List<Path> paths) {
if (!revealPaths(paths)) {
LOG.warn("No service provider to reveal files found.");
copyPathsToClipboard(paths);
}
}
/**
* Reveals the paths over the {@link RevealPathService} in the file system
*
* @param paths List of Paths to reveal
* @return true, if at least one service provider was present, false otherwise
*/
private boolean revealPaths(List<Path> paths) {
return RevealPathService.get().findAny().map(s -> {
paths.forEach(path -> {
try {
s.reveal(path);
} catch (RevealFailedException e) {
LOG.error("Revealing ciphertext file failed.", e);
}
});
return true;
}).orElse(false);
}
private void copyPathsToClipboard(List<Path> paths) {
StringBuilder clipboardString = new StringBuilder();
paths.forEach(p -> clipboardString.append(p.toString()).append("\n"));
Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, clipboardString.toString()));
ciphertextPathsCopied.setValue(true);
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
ciphertextPathsCopied.set(false);
});
}
/* Getter/Setter */
public ReadOnlyObjectProperty<Vault> vaultProperty() {
@@ -64,4 +195,11 @@ public class VaultDetailUnlockedController implements FxController {
return vault.get();
}
public BooleanProperty ciphertextPathsCopiedProperty() {
return ciphertextPathsCopied;
}
public boolean isCiphertextPathsCopied() {
return ciphertextPathsCopied.get();
}
}

View File

@@ -3,26 +3,43 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.Set;
import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
@@ -31,6 +48,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
@MainWindowScoped
public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage mainWindow;
private final ObservableList<Vault> vaults;
@@ -39,17 +57,21 @@ public class VaultListController implements FxController {
private final AddVaultWizardComponent.Builder addVaultWizard;
private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
private final VaultListManager vaultListManager;
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public ListView<Vault> vaultList;
public StackPane root;
@Inject
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue, VaultListManager vaultListManager) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
this.addVaultWizard = addVaultWizard;
this.removeVaultDialogue = removeVaultDialogue;
this.vaultListManager = vaultListManager;
this.emptyVaultList = Bindings.isEmpty(vaults);
@@ -100,6 +122,11 @@ public class VaultListController implements FxController {
keyEvent.consume();
}
});
root.setOnDragEntered(this::handleDragEvent);
root.setOnDragOver(this::handleDragEvent);
root.setOnDragDropped(this::handleDragEvent);
root.setOnDragExited(this::handleDragEvent);
}
private void deselect(MouseEvent released) {
@@ -128,6 +155,47 @@ public class VaultListController implements FxController {
}
}
private void handleDragEvent(DragEvent event) {
if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
if (draggingVaultOver.get()) {
event.acceptTransferModes(TransferMode.ANY);
}
} else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Set<Path> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
if (!vaultPaths.isEmpty()) {
vaultPaths.forEach(this::addVault);
}
event.setDropCompleted(!vaultPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingVaultOver.set(false);
}
}
private boolean containsVault(Path path) {
try {
if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
path = path.getParent();
}
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
} catch (IOException e) {
return false;
}
}
private void addVault(Path pathToVault) {
try {
if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
vaultListManager.add(pathToVault.getParent());
} else {
vaultListManager.add(pathToVault);
}
} catch (IOException e) {
LOG.debug("Not a vault: {}", pathToVault);
}
}
// Getter and Setter
public BooleanBinding emptyVaultListProperty() {
@@ -138,4 +206,13 @@ public class VaultListController implements FxController {
return emptyVaultList.get();
}
public BooleanProperty draggingVaultOverProperty() {
return draggingVaultOver;
}
public boolean isDraggingVaultOver() {
return draggingVaultOver.get();
}
}