From 77c0a3e26564aa2358865d409249f5b55d71ee2f Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 7 Jan 2023 15:27:35 +0100 Subject: [PATCH 1/4] Update build.sh to .github/workflows/appimage.yml --- dist/linux/appimage/build.sh | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index acd5e9e35..dcb6f530d 100755 --- a/dist/linux/appimage/build.sh +++ b/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 -Pdependency-check,linux -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 @@ -77,11 +82,16 @@ mv jni/x86_64-Linux/* Cryptomator.AppDir/lib/app/libjffi.so rm -r jni/x86_64-Linux # load AppImageTool -curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o /tmp/appimagetool.AppImage -chmod +x /tmp/appimagetool.AppImage +curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage +chmod +x appimagetool.AppImage +./appimagetool.AppImage --appimage-extract # create AppImage -/tmp/appimagetool.AppImage \ - Cryptomator.AppDir \ - cryptomator-SNAPSHOT-x86_64.AppImage \ - -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' +./squashfs-root/AppRun Cryptomator.AppDir 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 appimagetool.AppImage jni runtime squashfs-root; rm launcher-gtk2.properties" +echo "" \ No newline at end of file From 0e0e00f11643fb289c63d4092fcf85269d0e098f Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 8 Jan 2023 14:10:38 +0100 Subject: [PATCH 2/4] Remove stuff not needed when run locally From: changes requested for PR #2624 --- dist/linux/appimage/build.sh | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index dcb6f530d..607423dcf 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -14,7 +14,7 @@ SEMVER_STR=${VERSION} mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR} # compile -mvn -B -f ../../../pom.xml clean package -Pdependency-check,linux -DskipTests +mvn -B -f ../../../pom.xml clean package -Plinux -DskipTests cp ../../../LICENSE.txt ../../../target cp ../launcher.sh ../../../target cp ../../../target/cryptomator-*.jar ../../../target/mods @@ -82,16 +82,17 @@ mv jni/x86_64-Linux/* Cryptomator.AppDir/lib/app/libjffi.so rm -r jni/x86_64-Linux # load AppImageTool -curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage -chmod +x appimagetool.AppImage -./appimagetool.AppImage --appimage-extract +curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o /tmp/appimagetool.AppImage +chmod +x /tmp/appimagetool.AppImage # create AppImage -./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${SEMVER_STR}-x86_64.AppImage \ --u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' +/tmp/appimagetool.AppImage \ + Cryptomator.AppDir \ + 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 appimagetool.AppImage jni runtime squashfs-root; rm launcher-gtk2.properties" +echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage" echo "" \ No newline at end of file From 5beeeae27fc7fec335912189a769cedf75e508cf Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Jan 2023 18:47:41 +0100 Subject: [PATCH 3/4] Sign integrations.dll during build --- .github/workflows/win-exe.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index f36dd6d0d..ae95e4278 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -116,6 +116,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: @@ -126,6 +129,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 From d6388d6205298b4fb15e2b10da553b54ae0114d5 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 20 Jan 2023 10:46:00 +0100 Subject: [PATCH 4/4] 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 --- pom.xml | 10 +- .../org/cryptomator/common/vaults/Vault.java | 17 +++ .../ui/controls/FontAwesome5Icon.java | 1 + .../ui/mainwindow/MainWindowController.java | 90 +---------- .../VaultDetailUnlockedController.java | 140 +++++++++++++++++- .../ui/mainwindow/VaultListController.java | 79 +++++++++- src/main/resources/css/dark_theme.css | 47 ++++-- src/main/resources/css/light_theme.css | 47 ++++-- src/main/resources/fxml/main_window.fxml | 26 +--- .../resources/fxml/vault_detail_unlocked.fxml | 18 ++- src/main/resources/fxml/vault_list.fxml | 48 +++--- src/main/resources/i18n/strings.properties | 7 +- 12 files changed, 367 insertions(+), 163 deletions(-) diff --git a/pom.xml b/pom.xml index ce5230f38..b7ef1a0cd 100644 --- a/pom.xml +++ b/pom.xml @@ -28,11 +28,11 @@ 2.1.1 - 2.5.3 - 1.2.0-beta3 - 1.1.2 - 1.1.2 - 1.1.0 + 2.6.1 + 1.2.0-beta4 + 1.2.0-beta1 + 1.2.0-beta1 + 1.2.0-beta1 1.3.4 1.3.3 1.2.8 diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 54d37d5e3..898bf1a51 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -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()); } diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index ea6ba00d3..997bfa41f 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/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"), // diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index c81aff125..b2c912834 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/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 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 selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) { + public MainWindowController(@MainWindow Stage window, ObjectProperty 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 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(); - } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index 63d88a6a4..4bbfcce19 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -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; private final FxApplicationWindows appWindows; private final VaultService vaultService; + private final WrongFileAlertComponent.Builder wrongFileAlert; private final Stage mainWindow; + private final ResourceBundle resourceBundle; private final LoadingCache 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, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) { + public VaultDetailUnlockedController(ObjectProperty 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 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 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 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 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 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 vaultProperty() { @@ -64,4 +195,11 @@ public class VaultDetailUnlockedController implements FxController { return vault.get(); } + public BooleanProperty ciphertextPathsCopiedProperty() { + return ciphertextPathsCopied; + } + + public boolean isCiphertextPathsCopied() { + return ciphertextPathsCopied.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index adb9a961b..f0aadfdfc 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/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 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 vaultList; + public StackPane root; @Inject - VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) { + VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty 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 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(); + } + + } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 86467bb1a..fe510d420 100644 --- a/src/main/resources/css/dark_theme.css +++ b/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; +} diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index ddc872eb2..4e47af6df 100644 --- a/src/main/resources/css/light_theme.css +++ b/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; +} diff --git a/src/main/resources/fxml/main_window.fxml b/src/main/resources/fxml/main_window.fxml index 91e7512a4..2796455d3 100644 --- a/src/main/resources/fxml/main_window.fxml +++ b/src/main/resources/fxml/main_window.fxml @@ -1,10 +1,6 @@ - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index 29b11ed17..185bbefa8 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -35,7 +35,23 @@ - + + + + + + - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 8c12e81ae..1ea8e9ca2 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -338,9 +338,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ā€¦ @@ -370,6 +367,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