Parcourir la source

Merge branch 'develop' into feature/mount-provider

# Conflicts:
#	pom.xml
#	src/main/java/org/cryptomator/common/vaults/Vault.java
#	src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
Armin Schrenk il y a 2 ans
Parent
commit
90408504e2

+ 7 - 0
.github/workflows/win-exe.yml

@@ -118,6 +118,9 @@ jobs:
       - name: Fix permissions
         run: attrib -r appdir/Cryptomator/Cryptomator.exe
         shell: pwsh
+      - name: Extract integrations DLL for code signing
+        shell: pwsh
+        run: gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --extract integrations.dll }
       - name: Codesign
         uses: skymatic/code-sign-action@v2
         with:
@@ -128,6 +131,10 @@ jobs:
           timestampUrl: 'http://timestamp.digicert.com'
           folder: appdir/Cryptomator
           recursive: true
+      - name: Repack signed DLL into jar
+        shell: pwsh
+        run: |
+          gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --update integrations.dll; Remove-Item integrations.dll}
       - name: Generate license for MSI
         run: >
           mvn -B license:add-third-party

+ 18 - 7
dist/linux/appimage/build.sh

@@ -11,15 +11,20 @@ command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; }
 VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout)
 SEMVER_STR=${VERSION}
 
+mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR}
+
 # compile
-mvn -B -f ../../../pom.xml clean package -DskipTests -Plinux
+mvn -B -f ../../../pom.xml clean package -Plinux -DskipTests
+cp ../../../LICENSE.txt ../../../target
+cp ../launcher.sh ../../../target
 cp ../../../target/cryptomator-*.jar ../../../target/mods
 
 # add runtime
 ${JAVA_HOME}/bin/jlink \
+    --verbose \
     --output runtime \
     --module-path "${JAVA_HOME}/jmods" \
-    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
+    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
     --strip-native-commands \
     --no-header-files \
     --no-man-pages \
@@ -27,7 +32,7 @@ ${JAVA_HOME}/bin/jlink \
     --compress=1
 
 # create app dir
-envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
+envsubst '${SEMVER_STR} ${REVISION_NUM}' < ../launcher-gtk2.properties > launcher-gtk2.properties
 ${JAVA_HOME}/bin/jpackage \
     --verbose \
     --type app-image \
@@ -35,7 +40,7 @@ ${JAVA_HOME}/bin/jpackage \
     --input ../../../target/libs \
     --module-path ../../../target/mods \
     --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator \
-    --dest . \
+    --dest appdir \
     --name Cryptomator \
     --vendor "Skymatic GmbH" \
     --copyright "(C) 2016 - 2023 Skymatic GmbH" \
@@ -46,6 +51,7 @@ ${JAVA_HOME}/bin/jpackage \
     --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \
     --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \
     --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \
+    --java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\"" \
     --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \
     --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \
     --java-options "-Dcryptomator.showTrayIcon=false" \
@@ -54,9 +60,8 @@ ${JAVA_HOME}/bin/jpackage \
     --resource-dir ../resources
 
 # transform AppDir
-mv Cryptomator Cryptomator.AppDir
+mv appdir/Cryptomator Cryptomator.AppDir
 cp -r resources/AppDir/* Cryptomator.AppDir/
-chmod +x Cryptomator.AppDir/lib/runtime/bin/java
 envsubst '${REVISION_NO}' < resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh
 cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png
 cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png
@@ -83,5 +88,11 @@ chmod +x /tmp/appimagetool.AppImage
 # create AppImage
 /tmp/appimagetool.AppImage \
     Cryptomator.AppDir \
-    cryptomator-SNAPSHOT-x86_64.AppImage \
+    cryptomator-${SEMVER_STR}-x86_64.AppImage  \
     -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync'
+
+echo ""
+echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-x86_64.AppImage"
+echo ""
+echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage"
+echo ""

+ 5 - 5
pom.xml

@@ -29,11 +29,11 @@
 
 		<!-- cryptomator dependencies -->
 		<cryptomator.cryptolib.version>2.1.1</cryptomator.cryptolib.version>
-		<cryptomator.cryptofs.version>2.5.3</cryptomator.cryptofs.version>
-		<cryptomator.integrations.version>1.2.0-beta3</cryptomator.integrations.version>
-		<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
-		<cryptomator.integrations.mac.version>1.1.2</cryptomator.integrations.mac.version>
-		<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
+		<cryptomator.cryptofs.version>2.6.1</cryptomator.cryptofs.version>
+		<cryptomator.integrations.version>1.2.0-beta4</cryptomator.integrations.version>
+		<cryptomator.integrations.win.version>1.2.0-beta1</cryptomator.integrations.win.version>
+		<cryptomator.integrations.mac.version>1.2.0-beta1</cryptomator.integrations.mac.version>
+		<cryptomator.integrations.linux.version>1.2.0-beta1</cryptomator.integrations.linux.version>
 		<cryptomator.fuse.version>2.0.0-beta3</cryptomator.fuse.version>
 		<cryptomator.dokany.version>2.0.0-beta2</cryptomator.dokany.version>
 		<cryptomator.webdav.version>2.0.0-beta2</cryptomator.webdav.version>

+ 16 - 2
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -67,7 +67,6 @@ public class Vault {
 	private final BooleanBinding needsMigration;
 	private final BooleanBinding unknownError;
 	private final ObjectBinding<Mountpoint> mountPoint;
-	private final WindowsDriveLetters windowsDriveLetters;
 	private final Mounter mounter;
 	private final BooleanProperty showingStats;
 
@@ -89,7 +88,6 @@ public class Vault {
 		this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
 		this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
 		this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
-		this.windowsDriveLetters = windowsDriveLetters;
 		this.mounter = mounter;
 		this.showingStats = new SimpleBooleanProperty(false);
 	}
@@ -316,6 +314,22 @@ 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 VaultConfigCache getVaultConfigCache() {
 		return configCache;
 	}

+ 1 - 0
src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java

@@ -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"), //

+ 2 - 88
src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java

@@ -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();
-	}
 }

+ 141 - 8
src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java

@@ -1,56 +1,86 @@
 package org.cryptomator.ui.mainwindow;
 
+import com.google.common.base.Preconditions;
 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.common.vaults.VaultState;
 import org.cryptomator.integrations.mount.Mountpoint;
+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.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.binding.StringBinding;
+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.beans.value.ObservableValue;
 import javafx.fxml.FXML;
+import javafx.scene.control.Button;
 import javafx.scene.input.Clipboard;
 import javafx.scene.input.ClipboardContent;
+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.net.URI;
+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 ObservableValue<Boolean> accessibleViaPath;
 	private final ObservableValue<Boolean> accessibleViaUri;
 	private final ObservableValue<String> mountPoint;
+	private final BooleanProperty draggingOver = new SimpleBooleanProperty();
+	private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
+
+	//FXML
+	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;
 		var mp = vault.flatMap(Vault::mountPointProperty);
 		this.accessibleViaPath = mp.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
 		this.accessibleViaUri = mp.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
 		this.mountPoint = mp.map(m -> {
-			if(m instanceof Mountpoint.WithPath mwp) {
+			if (m instanceof Mountpoint.WithPath mwp) {
 				return mwp.path().toString();
 			} else {
 				return m.uri().toASCIIString();
@@ -58,6 +88,33 @@ public class VaultDetailUnlockedController implements FxController {
 		});
 	}
 
+	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();
 	}
@@ -69,7 +126,6 @@ public class VaultDetailUnlockedController implements FxController {
 
 	@FXML
 	public void copyMountUri() {
-		//TODO: add popup that conent is copied
 		ClipboardContent clipboardContent = new ClipboardContent();
 		clipboardContent.putString(mountPoint.getValue());
 		Clipboard.getSystemClipboard().setContent(clipboardContent);
@@ -85,6 +141,77 @@ public class VaultDetailUnlockedController implements FxController {
 		vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
 	}
 
+	@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);
+		}
+	}
+
+	private boolean startsWithVaultAccessPoint(Path path) {
+		return path.startsWith(Path.of(mountPoint.getValue()));
+	}
+
+	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 = mountPoint.getValue();
+			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() {
@@ -119,5 +246,11 @@ public class VaultDetailUnlockedController implements FxController {
 		return mountPoint.getValue();
 	}
 
+	public BooleanProperty ciphertextPathsCopiedProperty() {
+		return ciphertextPathsCopied;
+	}
 
+	public boolean isCiphertextPathsCopied() {
+		return ciphertextPathsCopied.get();
+	}
 }

+ 78 - 1
src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java

@@ -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();
+	}
+
+
 }

+ 37 - 10
src/main/resources/css/dark_theme.css

@@ -204,16 +204,6 @@
 	-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
 }
 
-.main-window .drag-n-drop-indicator {
-	-fx-border-color: SECONDARY;
-	-fx-border-width: 3px;
-}
-
-.main-window .drag-n-drop-indicator .drag-n-drop-header {
-	-fx-background-color: SECONDARY;
-	-fx-padding: 3px;
-}
-
 /*******************************************************************************
  *                                                                             *
  * TabPane                                                                     *
@@ -884,3 +874,40 @@
 	-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
 	-fx-stroke: transparent;
 }
+
+/*******************************************************************************
+ *                                                                             *
+ * Drag and Drop                                                               *
+ *                                                                             *
+ ******************************************************************************/
+
+.drag-n-drop-border {
+	-fx-border-color: SECONDARY;
+	-fx-border-width: 3px;
+}
+
+.button.drag-n-drop {
+	-fx-background-color: CONTROL_BG_NORMAL;
+	-fx-background-insets: 0;
+	-fx-padding: 1.4em 1em 1.4em 1em;
+	-fx-text-fill: TEXT_FILL_MUTED;
+	-fx-font-size: 0.8em;
+	-fx-border-color: CONTROL_BORDER_NORMAL;
+	-fx-border-radius: 4px;
+	-fx-border-style: dashed inside;
+	-fx-border-width: 1px;
+}
+
+.button.drag-n-drop:focused {
+	-fx-border-color: CONTROL_BORDER_FOCUSED;
+}
+
+.button.drag-n-drop:armed {
+	-fx-background-color: CONTROL_BG_ARMED;
+}
+
+.button.drag-n-drop.active {
+	-fx-border-color: SECONDARY;
+	-fx-border-style: solid inside;
+	-fx-border-width: 1px;
+}

+ 37 - 10
src/main/resources/css/light_theme.css

@@ -203,16 +203,6 @@
 	-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
 }
 
-.main-window .drag-n-drop-indicator {
-	-fx-border-color: SECONDARY;
-	-fx-border-width: 3px;
-}
-
-.main-window .drag-n-drop-indicator .drag-n-drop-header {
-	-fx-background-color: SECONDARY;
-	-fx-padding: 3px;
-}
-
 /*******************************************************************************
  *                                                                             *
  * TabPane                                                                     *
@@ -883,3 +873,40 @@
 	-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
 	-fx-stroke: transparent;
 }
+
+/*******************************************************************************
+ *                                                                             *
+ * Drag and Drop                                                               *
+ *                                                                             *
+ ******************************************************************************/
+
+.drag-n-drop-border {
+	-fx-border-color: SECONDARY;
+	-fx-border-width: 3px;
+}
+
+.button.drag-n-drop {
+	-fx-background-color: CONTROL_BG_NORMAL;
+	-fx-background-insets: 0;
+	-fx-padding: 1.4em 1em 1.4em 1em;
+	-fx-text-fill: TEXT_FILL_MUTED;
+	-fx-font-size: 0.8em;
+	-fx-border-color: CONTROL_BORDER_NORMAL;
+	-fx-border-radius: 4px;
+	-fx-border-style: dashed inside;
+	-fx-border-width: 1px;
+}
+
+.button.drag-n-drop:focused {
+	-fx-border-color: CONTROL_BORDER_FOCUSED;
+}
+
+.button.drag-n-drop:armed {
+	-fx-background-color: CONTROL_BG_ARMED;
+}
+
+.button.drag-n-drop.active {
+	-fx-border-color: SECONDARY;
+	-fx-border-style: solid inside;
+	-fx-border-width: 1px;
+}

+ 4 - 22
src/main/resources/fxml/main_window.fxml

@@ -1,10 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
-<?import javafx.scene.control.Label?>
 <?import javafx.scene.control.SplitPane?>
-<?import javafx.scene.layout.HBox?>
-<?import javafx.scene.layout.Region?>
 <?import javafx.scene.layout.StackPane?>
 <?import javafx.scene.layout.VBox?>
 <StackPane xmlns:fx="http://javafx.com/fxml"
@@ -14,24 +10,10 @@
 		   styleClass="main-window">
 	<VBox minWidth="650">
 		<fx:include source="main_window_title.fxml" VBox.vgrow="NEVER"/>
-		<StackPane VBox.vgrow="ALWAYS">
-			<SplitPane dividerPositions="0.33" orientation="HORIZONTAL">
-				<fx:include source="vault_list.fxml" SplitPane.resizableWithParent="false"/>
-				<fx:include source="vault_detail.fxml" SplitPane.resizableWithParent="true"/>
-			</SplitPane>
-
-			<VBox styleClass="drag-n-drop-indicator" visible="${controller.draggingOver}" alignment="TOP_CENTER">
-				<HBox visible="${!controller.draggingVaultOver}" managed="${!controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
-					<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
-					<Label text="%main.dropZone.unknownDragboardContent"/>
-				</HBox>
-				<HBox visible="${controller.draggingVaultOver}" managed="${controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
-					<FontAwesome5IconView glyph="CHECK"/>
-					<Label text="%main.dropZone.dropVault"/>
-				</HBox>
-				<Region VBox.vgrow="ALWAYS"/>
-			</VBox>
-		</StackPane>
+		<SplitPane dividerPositions="0.33" orientation="HORIZONTAL" VBox.vgrow="ALWAYS">
+			<fx:include source="vault_list.fxml" SplitPane.resizableWithParent="false"/>
+			<fx:include source="vault_detail.fxml" SplitPane.resizableWithParent="true"/>
+		</SplitPane>
 	</VBox>
 	<fx:include source="main_window_resize.fxml"/>
 </StackPane>

+ 24 - 1
src/main/resources/fxml/vault_detail_unlocked.fxml

@@ -5,6 +5,8 @@
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.Region?>
 <?import javafx.scene.layout.VBox?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.geometry.Insets?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.mainwindow.VaultDetailUnlockedController"
@@ -41,7 +43,28 @@
 
 	<Region VBox.vgrow="ALWAYS"/>
 
-	<HBox alignment="BOTTOM_RIGHT">
+	<HBox alignment="BOTTOM_CENTER">
+		<HBox visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
+			<padding>
+				<Insets topRightBottomLeft="0"/>
+			</padding>
+			<Button fx:id="dropZone" styleClass="drag-n-drop" text="%main.vaultDetail.locateEncryptedFileBtn" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${!controller.ciphertextPathsCopied}" managed="${!controller.ciphertextPathsCopied}">
+				<graphic>
+					<FontAwesome5IconView glyph="FILE_DOWNLOAD" glyphSize="15"/>
+				</graphic>
+				<tooltip>
+					<Tooltip text="%main.vaultDetail.locateEncryptedFileBtn.tooltip"/>
+				</tooltip>
+			</Button>
+			<Button styleClass="drag-n-drop" text="%main.vaultDetail.encryptedPathsCopied" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${controller.ciphertextPathsCopied}" managed="${controller.ciphertextPathsCopied}">
+				<graphic>
+					<FontAwesome5IconView glyph="CHECK" glyphSize="15"/>
+				</graphic>
+			</Button>
+		</HBox>
+
+		<Region HBox.hgrow="ALWAYS"/>
+
 		<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM">
 			<graphic>
 				<VBox spacing="6">

+ 26 - 22
src/main/resources/fxml/vault_list.fxml

@@ -8,25 +8,29 @@
 <?import javafx.scene.layout.StackPane?>
 <?import javafx.scene.layout.VBox?>
 <?import javafx.scene.shape.Arc?>
-<VBox xmlns:fx="http://javafx.com/fxml"
-	  xmlns="http://javafx.com/javafx"
-	  fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
-	  minWidth="206">
-	<StackPane VBox.vgrow="ALWAYS">
-		<ListView fx:id="vaultList" editable="true" fixedCellSize="60">
-			<contextMenu>
-				<fx:include source="vault_list_contextmenu.fxml"/>
-			</contextMenu>
-		</ListView>
-		<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
-			<Region VBox.vgrow="ALWAYS"/>
-			<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" textAlignment="CENTER" wrapText="true"/>
-			<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
-		</VBox>
-	</StackPane>
-	<Button styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" onAction="#didClickAddVault" alignment="BASELINE_CENTER" maxWidth="Infinity">
-		<graphic>
-			<FontAwesome5IconView glyph="PLUS"/>
-		</graphic>
-	</Button>
-</VBox>
+<StackPane xmlns:fx="http://javafx.com/fxml"
+		   xmlns="http://javafx.com/javafx"
+		   fx:id="root"
+		   fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
+		   minWidth="206">
+	<VBox>
+		<StackPane VBox.vgrow="ALWAYS">
+			<ListView fx:id="vaultList" editable="true" fixedCellSize="60">
+				<contextMenu>
+					<fx:include source="vault_list_contextmenu.fxml"/>
+				</contextMenu>
+			</ListView>
+			<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
+				<Region VBox.vgrow="ALWAYS"/>
+				<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" textAlignment="CENTER" wrapText="true"/>
+				<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
+			</VBox>
+		</StackPane>
+		<Button styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" onAction="#didClickAddVault" alignment="BASELINE_CENTER" maxWidth="Infinity">
+			<graphic>
+				<FontAwesome5IconView glyph="PLUS"/>
+			</graphic>
+		</Button>
+	</VBox>
+	<Region styleClass="drag-n-drop-border" visible="${controller.draggingVaultOver}"/>
+</StackPane>

+ 4 - 3
src/main/resources/i18n/strings.properties

@@ -343,9 +343,6 @@ main.minimizeBtn.tooltip=Minimize
 main.preferencesBtn.tooltip=Preferences
 main.debugModeEnabled.tooltip=Debug mode is enabled
 main.supporterCertificateMissing.tooltip=Please consider donating
-## Drag 'n' Drop
-main.dropZone.dropVault=Add this vault
-main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window
 ## Vault List
 main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault
 main.vaultlist.contextMenu.remove=Remove…
@@ -376,6 +373,10 @@ main.vaultDetail.throughput.idle=idle
 main.vaultDetail.throughput.kbps=%.1f kiB/s
 main.vaultDetail.throughput.mbps=%.1f MiB/s
 main.vaultDetail.stats=Vault Statistics
+main.vaultDetail.locateEncryptedFileBtn=Locate Encrypted File
+main.vaultDetail.locateEncryptedFileBtn.tooltip=Choose a file from your vault to locate its encrypted counterpart
+main.vaultDetail.encryptedPathsCopied=Paths Copied to Clipboard!
+main.vaultDetail.filePickerTitle=Select File Inside Vault
 ### Missing
 main.vaultDetail.missing.info=Cryptomator could not find a vault at this path.
 main.vaultDetail.missing.recheck=Recheck