Browse Source

Merge pull request #3494 from cryptomator/feature/quick-access

Feature: Add unlocked vaults to an quick access area
Armin Schrenk 7 months ago
parent
commit
a20667a156

+ 1 - 1
.github/workflows/win-exe.yml

@@ -113,7 +113,7 @@ jobs:
           --copyright "(C) 2016 - 2024 Skymatic GmbH"
           --app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
           --java-options "--enable-preview"
-          --java-options "--enable-native-access=org.cryptomator.jfuse.win"
+          --java-options "--enable-native-access=org.cryptomator.jfuse.win,org.cryptomator.integrations.win"
           --java-options "-Xss5m"
           --java-options "-Xmx256m"
           --java-options "-Dcryptomator.appVersion=\"${{ needs.get-version.outputs.semVerStr }}\""

File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows.xml


File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows_Dev.xml


+ 1 - 1
dist/win/build.ps1

@@ -100,7 +100,7 @@ if ($clean -and (Test-Path -Path $appPath)) {
 	--vendor $Vendor `
 	--copyright $copyright `
 	--java-options "--enable-preview" `
-	--java-options "--enable-native-access=org.cryptomator.jfuse.win" `
+	--java-options "--enable-native-access=org.cryptomator.jfuse.win,org.cryptomator.integrations.win" `
 	--java-options "-Xss5m" `
 	--java-options "-Xmx256m" `
 	--java-options "-Dcryptomator.appVersion=`"$semVerNo`"" `

+ 3 - 3
pom.xml

@@ -34,10 +34,10 @@
 
 		<!-- cryptomator dependencies -->
 		<cryptomator.cryptofs.version>2.6.9</cryptomator.cryptofs.version>
-		<cryptomator.integrations.version>1.3.1</cryptomator.integrations.version>
-		<cryptomator.integrations.win.version>1.2.5</cryptomator.integrations.win.version>
+		<cryptomator.integrations.version>1.4.0-beta2</cryptomator.integrations.version>
+		<cryptomator.integrations.win.version>1.2.5</cryptomator.integrations.win.version> <!-- TODO update to 1.3.0 once released -->
 		<cryptomator.integrations.mac.version>1.2.4</cryptomator.integrations.mac.version>
-		<cryptomator.integrations.linux.version>1.4.5</cryptomator.integrations.linux.version>
+		<cryptomator.integrations.linux.version>1.4.5</cryptomator.integrations.linux.version> <!-- TODO update to 1.5.0 once released -->
 		<cryptomator.fuse.version>5.0.0</cryptomator.fuse.version>
 		<cryptomator.webdav.version>2.0.6</cryptomator.webdav.version>
 

+ 15 - 1
src/main/java/org/cryptomator/common/settings/Settings.java

@@ -37,12 +37,18 @@ public class Settings {
 	static final boolean DEFAULT_START_HIDDEN = false;
 	static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
 	static final boolean DEFAULT_USE_KEYCHAIN = true;
+	static final boolean DEFAULT_USE_QUICKACCESS = true;
 	static final int DEFAULT_PORT = 42427;
 	static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
 	static final boolean DEFAULT_DEBUG_MODE = false;
 	static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
 	@Deprecated // to be changed to "whatever is available" eventually
-	static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
+	static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : //
+			SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : //
+					"org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
+	static final String DEFAULT_QUICKACCESS_SERVICE = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.quickaccess.ExplorerQuickAccessService" : //
+			SystemUtils.IS_OS_LINUX ? "org.cryptomator.linux.quickaccess.NautilusBookmarks" : null;
+
 	static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
 	static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
 	public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
@@ -57,6 +63,8 @@ public class Settings {
 	public final BooleanProperty debugMode;
 	public final ObjectProperty<UiTheme> theme;
 	public final StringProperty keychainProvider;
+	public final BooleanProperty useQuickAccess;
+	public final StringProperty quickAccessService;
 	public final ObjectProperty<NodeOrientation> userInterfaceOrientation;
 	public final StringProperty licenseKey;
 	public final BooleanProperty showMinimizeButton;
@@ -89,6 +97,7 @@ public class Settings {
 		this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
 		this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
 		this.useKeychain = new SimpleBooleanProperty(this, "useKeychain", json.useKeychain);
+		this.useQuickAccess = new SimpleBooleanProperty(this, "addToQuickAccess", json.useQuickAccess);
 		this.port = new SimpleIntegerProperty(this, "webDavPort", json.port);
 		this.numTrayNotifications = new SimpleIntegerProperty(this, "numTrayNotifications", json.numTrayNotifications);
 		this.debugMode = new SimpleBooleanProperty(this, "debugMode", json.debugMode);
@@ -104,6 +113,7 @@ public class Settings {
 		this.windowHeight = new SimpleIntegerProperty(this, "windowHeight", json.windowHeight);
 		this.language = new SimpleStringProperty(this, "language", json.language);
 		this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
+		this.quickAccessService = new SimpleStringProperty(this, "quickAccessService", json.quickAccessService);
 		this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
 
 		this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
@@ -116,6 +126,7 @@ public class Settings {
 		startHidden.addListener(this::somethingChanged);
 		autoCloseVaults.addListener(this::somethingChanged);
 		useKeychain.addListener(this::somethingChanged);
+		useQuickAccess.addListener(this::somethingChanged);
 		port.addListener(this::somethingChanged);
 		numTrayNotifications.addListener(this::somethingChanged);
 		debugMode.addListener(this::somethingChanged);
@@ -131,6 +142,7 @@ public class Settings {
 		windowHeight.addListener(this::somethingChanged);
 		language.addListener(this::somethingChanged);
 		mountService.addListener(this::somethingChanged);
+		quickAccessService.addListener(this::somethingChanged);
 		lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
 	}
 
@@ -170,6 +182,7 @@ public class Settings {
 		json.startHidden = startHidden.get();
 		json.autoCloseVaults = autoCloseVaults.get();
 		json.useKeychain = useKeychain.get();
+		json.useQuickAccess = useQuickAccess.get();
 		json.port = port.get();
 		json.numTrayNotifications = numTrayNotifications.get();
 		json.debugMode = debugMode.get();
@@ -185,6 +198,7 @@ public class Settings {
 		json.windowHeight = windowHeight.get();
 		json.language = language.get();
 		json.mountService = mountService.get();
+		json.quickAccessService = quickAccessService.get();
 		json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
 		return json;
 	}

+ 5 - 0
src/main/java/org/cryptomator/common/settings/SettingsJson.java

@@ -86,4 +86,9 @@ class SettingsJson {
 	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
 	Instant lastSuccessfulUpdateCheck = Settings.DEFAULT_TIMESTAMP;
 
+	@JsonProperty("useQuickAccess")
+	boolean useQuickAccess = Settings.DEFAULT_USE_QUICKACCESS;
+
+	@JsonProperty("quickAccessService")
+	String quickAccessService = Settings.DEFAULT_QUICKACCESS_SERVICE;
 }

+ 4 - 1
src/main/java/org/cryptomator/common/settings/SettingsProvider.java

@@ -58,7 +58,10 @@ public class SettingsProvider implements Supplier<Settings> {
 	}
 
 	private Settings load() {
-		Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElseGet(() -> Settings.create(env));
+		Settings settings = env.getSettingsPath() //
+				.flatMap(this::tryLoad) //
+				.findFirst() //
+				.orElseGet(() -> Settings.create(env));
 		settings.setSaveCmd(this::scheduleSave);
 		return settings;
 	}

+ 59 - 1
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -11,6 +11,7 @@ package org.cryptomator.common.vaults;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.Constants;
 import org.cryptomator.common.mount.Mounter;
+import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties;
@@ -23,6 +24,9 @@ import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.cryptomator.integrations.mount.MountFailedException;
 import org.cryptomator.integrations.mount.Mountpoint;
 import org.cryptomator.integrations.mount.UnmountFailedException;
+import org.cryptomator.integrations.quickaccess.QuickAccessService;
+import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,6 +58,7 @@ public class Vault {
 
 	private final VaultSettings vaultSettings;
 	private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
+	private final AtomicReference<QuickAccessService.QuickAccessEntry> quickAccessEntry;
 	private final VaultState state;
 	private final ObjectProperty<Exception> lastKnownException;
 	private final VaultConfigCache configCache;
@@ -67,6 +72,7 @@ public class Vault {
 	private final BooleanBinding unknownError;
 	private final ObjectBinding<Mountpoint> mountPoint;
 	private final Mounter mounter;
+	private final Settings settings;
 	private final BooleanProperty showingStats;
 
 	private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
@@ -78,7 +84,7 @@ public class Vault {
 		  VaultState state, //
 		  @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
 		  VaultStats stats, //
-		  Mounter mounter) {
+		  Mounter mounter, Settings settings) {
 		this.vaultSettings = vaultSettings;
 		this.configCache = configCache;
 		this.cryptoFileSystem = cryptoFileSystem;
@@ -94,7 +100,9 @@ public class Vault {
 		this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
 		this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
 		this.mounter = mounter;
+		this.settings = settings;
 		this.showingStats = new SimpleBooleanProperty(false);
+		this.quickAccessEntry = new AtomicReference<>(null);
 	}
 
 	// ******************************************************************************
@@ -154,6 +162,9 @@ public class Vault {
 			var rootPath = fs.getRootDirectories().iterator().next();
 			var mountHandle = mounter.mount(vaultSettings, rootPath);
 			success = this.mountHandle.compareAndSet(null, mountHandle);
+			if (settings.useQuickAccess.getValue()) {
+				addToQuickAccess();
+			}
 		} finally {
 			if (!success) {
 				destroyCryptoFileSystem();
@@ -178,6 +189,7 @@ public class Vault {
 			mountHandle.mountObj().close();
 			mountHandle.specialCleanup().run();
 		} finally {
+			removeFromQuickAccess();
 			destroyCryptoFileSystem();
 		}
 
@@ -185,6 +197,52 @@ public class Vault {
 		LOG.info("Locked vault '{}'", getDisplayName());
 	}
 
+	private synchronized void addToQuickAccess() {
+		if (quickAccessEntry.get() != null) {
+			//we don't throw an exception since we don't wanna block unlocking
+			LOG.warn("Vault already added to quick access area. Will be removed on next lock operation.");
+			return;
+		}
+
+		QuickAccessService.get() //
+				.filter(s -> s.getClass().getName().equals(settings.quickAccessService.getValue())) //
+				.findFirst() //
+				.ifPresentOrElse( //
+						this::addToQuickAccessInternal, //
+						() -> LOG.warn("Unable to add Vault to quick access area: Desired implementation not available.") //
+				);
+	}
+
+	private void addToQuickAccessInternal(@NotNull QuickAccessService s) {
+		if (getMountPoint() instanceof Mountpoint.WithPath mp) {
+			try {
+				var entry = s.add(mp.path(), getDisplayName());
+				quickAccessEntry.set(entry);
+			} catch (QuickAccessServiceException e) {
+				LOG.error("Adding vault to quick access area failed", e);
+			}
+		} else {
+			LOG.warn("Unable to add vault to quick access area: Vault is not mounted to local system path.");
+		}
+	}
+
+	private synchronized void removeFromQuickAccess() {
+		if (quickAccessEntry.get() == null) {
+			LOG.debug("Removing vault from quick access area: Entry not found, nothing to do.");
+			return;
+		}
+		removeFromQuickAccessInternal();
+	}
+
+	private void removeFromQuickAccessInternal() {
+		try {
+			quickAccessEntry.get().remove();
+			quickAccessEntry.set(null);
+		} catch (QuickAccessServiceException e) {
+			LOG.error("Removing vault from quick access area failed", e);
+		}
+	}
+
 	// ******************************************************************************
 	// Observable Properties
 	// *******************************************************************************

+ 46 - 10
src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java

@@ -4,7 +4,9 @@ import org.cryptomator.common.Environment;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.integrations.autostart.AutoStartProvider;
 import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
+import org.cryptomator.integrations.common.NamedServiceProvider;
 import org.cryptomator.integrations.keychain.KeychainAccessProvider;
+import org.cryptomator.integrations.quickaccess.QuickAccessService;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.slf4j.Logger;
@@ -30,12 +32,15 @@ public class GeneralPreferencesController implements FxController {
 	private final Stage window;
 	private final Settings settings;
 	private final Optional<AutoStartProvider> autoStartProvider;
+	private final List<QuickAccessService> quickAccessServices;
 	private final Application application;
 	private final Environment environment;
 	private final List<KeychainAccessProvider> keychainAccessProviders;
 	private final FxApplicationWindows appWindows;
 	public CheckBox useKeychainCheckbox;
 	public ChoiceBox<KeychainAccessProvider> keychainBackendChoiceBox;
+	public CheckBox useQuickAccessCheckbox;
+	public ChoiceBox<QuickAccessService> quickAccessServiceChoiceBox;
 	public CheckBox startHiddenCheckbox;
 	public CheckBox autoCloseVaultsCheckbox;
 	public CheckBox debugModeCheckbox;
@@ -48,6 +53,7 @@ public class GeneralPreferencesController implements FxController {
 		this.settings = settings;
 		this.autoStartProvider = autoStartProvider;
 		this.keychainAccessProviders = keychainAccessProviders;
+		this.quickAccessServices = QuickAccessService.get().toList();
 		this.application = application;
 		this.environment = environment;
 		this.appWindows = appWindows;
@@ -60,13 +66,21 @@ public class GeneralPreferencesController implements FxController {
 		debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode);
 		autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));
 
-		var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders);
+		var keychainSettingsConverter = new ServiceToSettingsConverter<>(keychainAccessProviders);
 		keychainBackendChoiceBox.getItems().addAll(keychainAccessProviders);
 		keychainBackendChoiceBox.setValue(keychainSettingsConverter.fromString(settings.keychainProvider.get()));
 		keychainBackendChoiceBox.setConverter(new KeychainProviderDisplayNameConverter());
 		Bindings.bindBidirectional(settings.keychainProvider, keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter);
 		useKeychainCheckbox.selectedProperty().bindBidirectional(settings.useKeychain);
 		keychainBackendChoiceBox.disableProperty().bind(useKeychainCheckbox.selectedProperty().not());
+
+		useQuickAccessCheckbox.selectedProperty().bindBidirectional(settings.useQuickAccess);
+		var quickAccessSettingsConverter = new ServiceToSettingsConverter<>(quickAccessServices);
+		quickAccessServiceChoiceBox.getItems().addAll(quickAccessServices);
+		quickAccessServiceChoiceBox.setValue(quickAccessSettingsConverter.fromString(settings.quickAccessService.get()));
+		quickAccessServiceChoiceBox.setConverter(new NamedServiceConverter<>());
+		Bindings.bindBidirectional(settings.quickAccessService, quickAccessServiceChoiceBox.valueProperty(), quickAccessSettingsConverter);
+		quickAccessServiceChoiceBox.disableProperty().bind(useQuickAccessCheckbox.selectedProperty().not());
 	}
 
 	public boolean isAutoStartSupported() {
@@ -91,6 +105,10 @@ public class GeneralPreferencesController implements FxController {
 		});
 	}
 
+	public boolean isSomeQuickAccessServiceAvailable() {
+		return !quickAccessServices.isEmpty();
+	}
+
 	@FXML
 	public void showLogfileDirectory() {
 		environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
@@ -116,29 +134,47 @@ public class GeneralPreferencesController implements FxController {
 
 	}
 
-	private static class KeychainProviderClassNameConverter extends StringConverter<KeychainAccessProvider> {
+	private static class NamedServiceConverter<T extends NamedServiceProvider> extends StringConverter<T> {
 
-		private final List<KeychainAccessProvider> keychainAccessProviders;
+		@Override
+		public String toString(T namedService) {
+			if (namedService == null) {
+				return null;
+			} else {
+				return namedService.getName();
+			}
+		}
 
-		public KeychainProviderClassNameConverter(List<KeychainAccessProvider> keychainAccessProviders) {
-			this.keychainAccessProviders = keychainAccessProviders;
+		@Override
+		public T fromString(String string) {
+			throw new UnsupportedOperationException();
+		}
+
+	}
+
+	private static class ServiceToSettingsConverter<T> extends StringConverter<T> {
+
+		private final List<T> services;
+
+		public ServiceToSettingsConverter(List<T> services) {
+			this.services = services;
 		}
 
 		@Override
-		public String toString(KeychainAccessProvider provider) {
-			if (provider == null) {
+		public String toString(T service) {
+			if (service == null) {
 				return null;
 			} else {
-				return provider.getClass().getName();
+				return service.getClass().getName();
 			}
 		}
 
 		@Override
-		public KeychainAccessProvider fromString(String string) {
+		public T fromString(String string) {
 			if (string == null) {
 				return null;
 			} else {
-				return keychainAccessProviders.stream().filter(provider -> provider.getClass().getName().equals(string)).findAny().orElse(null);
+				return services.stream().filter(provider -> provider.getClass().getName().equals(string)).findAny().orElse(null);
 			}
 		}
 	}

+ 4 - 0
src/main/resources/fxml/preferences_general.fxml

@@ -30,6 +30,10 @@
 			<ChoiceBox fx:id="keychainBackendChoiceBox"/>
 		</HBox>
 
+		<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.someQuickAccessServiceAvailable}" managed="${controller.someQuickAccessServiceAvailable}">
+			<CheckBox fx:id="useQuickAccessCheckbox" text="%preferences.general.quickAccessService"/>
+			<ChoiceBox fx:id="quickAccessServiceChoiceBox"/>
+		</HBox>
 		<Region VBox.vgrow="ALWAYS"/>
 
 		<HBox spacing="12" alignment="CENTER_LEFT">

+ 1 - 0
src/main/resources/i18n/strings.properties

@@ -288,6 +288,7 @@ preferences.general.debugLogging=Enable debug logging
 preferences.general.debugDirectory=Reveal log files
 preferences.general.autoStart=Launch Cryptomator on system start
 preferences.general.keychainBackend=Store passwords with
+preferences.general.quickAccessService=Add unlocked vaults to the quick access area
 ## Interface
 preferences.interface=Interface
 preferences.interface.theme=Look & Feel