+ * Created by {@link MountBuilder} + */ +public interface Mount extends AutoCloseable { + + /** + * Returns the absolute OS path, where this mount can be accessed. + * + * @return Absolute path to the mountpoint. + */ + Mountpoint getMountpoint(); + + /** + * Unmounts the mounted Volume. + *
+ * If possible, attempt a graceful unmount. + * + * @throws UnmountFailedException If the unmount was not successful. + * @see #unmountForced() + */ + void unmount() throws UnmountFailedException; + + /** + * If supported, force-unmount the volume. + * + * @throws UnmountFailedException If the unmount was not successful. + * @throws UnsupportedOperationException If {@link MountCapability#UNMOUNT_FORCED} is not supported + */ + default void unmountForced() throws UnmountFailedException { + throw new UnsupportedOperationException(); + } + + /** + * Unmounts (if required) and releases any resources. + * + * @throws UnmountFailedException Thrown if unmounting failed + * @throws IOException Thrown if cleaning up resources failed + */ + default void close() throws UnmountFailedException, IOException { + unmount(); + } + + +} diff --git a/src/main/java/org/cryptomator/integrations/mount/MountBuilder.java b/src/main/java/org/cryptomator/integrations/mount/MountBuilder.java new file mode 100644 index 0000000..7c9f903 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/mount/MountBuilder.java @@ -0,0 +1,128 @@ +package org.cryptomator.integrations.mount; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Range; + +import java.nio.file.Path; + +/** + * Builder to mount a filesystem. + *
+ * The setter may attempt to validate the input, but {@link #mount()} may still fail due to missing or invalid (combination of) options.
+ * This holds especially for {@link MountBuilder#setMountFlags(String)};
+ */
+public interface MountBuilder {
+
+ /**
+ * Sets the file system name.
+ *
+ * @param fileSystemName file system name
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#FILE_SYSTEM_NAME} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setFileSystemName(String fileSystemName) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Use the given host name as the loopback address.
+ *
+ * @param hostName string conforming with the uri host part
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#LOOPBACK_HOST_NAME} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setLoopbackHostName(String hostName) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Use the given TCP port of the loopback address.
+ *
+ * @param port Fixed TCP port or 0 to use a system-assigned port
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#LOOPBACK_PORT} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setLoopbackPort(@Range(from = 0, to = Short.MAX_VALUE) int port) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the mount point.
+ *
+ * Unless the mount service provider supports {@link MountCapability#MOUNT_TO_SYSTEM_CHOSEN_PATH}, setting a mount point is required.
+ *
+ * @param mountPoint Where to mount the volume
+ * @return this
+ */
+ @Contract("_ -> this")
+ default MountBuilder setMountpoint(Path mountPoint) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets mount flags.
+ *
+ * @param mountFlags Mount flags
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#MOUNT_FLAGS} is not supported
+ * @see MountService#getDefaultMountFlags()
+ */
+ @Contract("_ -> this")
+ default MountBuilder setMountFlags(String mountFlags) {
+ throw new UnsupportedOperationException();
+ }
+
+
+ /**
+ * Instructs the mount to be read-only.
+ *
+ * @param mountReadOnly Whether to mount read-only.
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#READ_ONLY} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setReadOnly(boolean mountReadOnly) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets a unique volume id.
+ *
+ * The volume id is used as a path component, thus must conform with the os-dependent path component restrictions.
+ *
+ * @param volumeId String conforming with the os-dependent path component restrictions
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#VOLUME_ID} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setVolumeId(String volumeId) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets a volume name.
+ *
+ * The volume name is intended to be human-readable. The input string might be altered to replace non-conforming characters and thus is not suited to identify the volume.
+ *
+ * @param volumeName String conforming with the os-dependent naming restrictions
+ * @return this
+ * @throws UnsupportedOperationException If {@link MountCapability#VOLUME_NAME} is not supported
+ */
+ @Contract("_ -> this")
+ default MountBuilder setVolumeName(String volumeName) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Mounts the file system.
+ *
+ * @return A mount handle
+ * @throws MountFailedException If mounting failed
+ */
+ @Contract(" -> new")
+ Mount mount() throws MountFailedException;
+
+}
diff --git a/src/main/java/org/cryptomator/integrations/mount/MountCapability.java b/src/main/java/org/cryptomator/integrations/mount/MountCapability.java
new file mode 100644
index 0000000..73ba871
--- /dev/null
+++ b/src/main/java/org/cryptomator/integrations/mount/MountCapability.java
@@ -0,0 +1,85 @@
+package org.cryptomator.integrations.mount;
+
+import java.nio.file.Path;
+
+/**
+ * Describes what aspects of the mount implementation can or should be used.
+ *
+ * This may be used to show or hide different configuration options depending on the chosen mount provider. + */ +public enum MountCapability { + /** + * The builder supports {@link MountBuilder#setFileSystemName(String)}. + */ + FILE_SYSTEM_NAME, + + /** + * The builder supports {@link MountBuilder#setLoopbackHostName(String)}. + */ + LOOPBACK_HOST_NAME, + + /** + * The service provider supports {@link MountService#getDefaultLoopbackPort()} + * and the builder requires {@link MountBuilder#setLoopbackPort(int)}. + */ + LOOPBACK_PORT, + + /** + * The service provider supports {@link MountService#getDefaultMountFlags()} + * and the builder requires {@link MountBuilder#setMountFlags(String)}. + */ + MOUNT_FLAGS, + + /** + * With the exception of a provider-supplied default mount point, the mount point must be an existing dir. + *
+ * This option is mutually exclusive with {@link #MOUNT_WITHIN_EXISTING_PARENT}. + * + * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH + */ + MOUNT_TO_EXISTING_DIR, + + /** + * With the exception of a provider-supplied default mount point, the mount point must be a non-existing + * child within an existing parent. + *
+ * This option is mutually exclusive with {@link #MOUNT_TO_EXISTING_DIR}.
+ *
+ * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH
+ */
+ MOUNT_WITHIN_EXISTING_PARENT,
+
+ /**
+ * The mount point may be a drive letter.
+ *
+ * @see #MOUNT_TO_EXISTING_DIR
+ * @see #MOUNT_WITHIN_EXISTING_PARENT
+ * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH
+ */
+ MOUNT_AS_DRIVE_LETTER,
+
+ /**
+ * The service provider supports suggesting a default mount point, if no mount point is set via {@link MountBuilder#setMountpoint(Path)}.
+ */
+ MOUNT_TO_SYSTEM_CHOSEN_PATH,
+
+ /**
+ * The builder supports {@link MountBuilder#setReadOnly(boolean)}.
+ */
+ READ_ONLY,
+
+ /**
+ * The mount supports {@link Mount#unmountForced()}.
+ */
+ UNMOUNT_FORCED,
+
+ /**
+ * The builder requires {@link MountBuilder#setVolumeId(String)}.
+ */
+ VOLUME_ID,
+
+ /**
+ * The builder supports {@link MountBuilder#setVolumeName(String)}.
+ */
+ VOLUME_NAME
+}
diff --git a/src/main/java/org/cryptomator/integrations/mount/MountFailedException.java b/src/main/java/org/cryptomator/integrations/mount/MountFailedException.java
new file mode 100644
index 0000000..c22556f
--- /dev/null
+++ b/src/main/java/org/cryptomator/integrations/mount/MountFailedException.java
@@ -0,0 +1,16 @@
+package org.cryptomator.integrations.mount;
+
+public class MountFailedException extends Exception {
+
+ public MountFailedException(String msg) {
+ super(msg);
+ }
+
+ public MountFailedException(Exception cause) {
+ super(cause);
+ }
+
+ public MountFailedException(String msg, Exception cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/org/cryptomator/integrations/mount/MountService.java b/src/main/java/org/cryptomator/integrations/mount/MountService.java
new file mode 100644
index 0000000..207f30e
--- /dev/null
+++ b/src/main/java/org/cryptomator/integrations/mount/MountService.java
@@ -0,0 +1,90 @@
+package org.cryptomator.integrations.mount;
+
+import org.cryptomator.integrations.common.IntegrationsLoader;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.Range;
+
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * A mechanism to mount a file system.
+ *
+ * @since 1.2.0
+ */
+public interface MountService {
+
+ /**
+ * Loads all supported mount providers.
+ *
+ * @return Stream of supported MountProviders (may be empty)
+ */
+ static Stream
+ * If the path points to a file, the parent of the file is opened and the file is selected in the file manager window.
+ * If the path points to a directory, the directory is opened and its content shown in the file manager window.
+ *
+ * @param p Path to reveal
+ * @throws RevealFailedException if revealing the path failed
+ */
+ void reveal(Path p) throws RevealFailedException;
+
+ /**
+ * Indicates, if this provider can be used.
+ *
+ * @return true, if this provider is supported in the current OS environment
+ * @implSpec This check needs to return fast and in constant time
+ */
+ boolean isSupported();
+
+}
diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
index 2d8b864..85f8c63 100644
--- a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
+++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
@@ -30,6 +30,14 @@ static Optional
@@ -40,4 +48,16 @@ static Optional
+ * This method is used to set up an event listener for when the menu is opened,
+ * e.g. so that the vault list can be updated to reflect volume mount state changes
+ * which occur while Cryptomator is in the system tray (and not open).
+ *
+ * @param listener
+ * @throws IllegalStateException thrown when adding listeners fails (i.e. there's no tray menu)
+ */
+ void onBeforeOpenMenu(Runnable listener);
+
}
diff --git a/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java b/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java
index a5a8db5..2463966 100644
--- a/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java
+++ b/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java
@@ -47,12 +47,15 @@ public void setup(@TempDir Path tmpDir) throws IOException {
@Test
@DisplayName("can load resources from both jars")
public void testForPluginDirWithPath() throws IOException {
- var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir);
- var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
- var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();
-
- Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
- Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
+ try (var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir);
+ var fooIn = cl.getResourceAsStream("foo.properties");
+ var barIn = cl.getResourceAsStream("bar.properties")) {
+ var fooContents = fooIn.readAllBytes();
+ var barContents = barIn.readAllBytes();
+
+ Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
+ Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
+ }
}
@Test
@@ -60,12 +63,15 @@ public void testForPluginDirWithPath() throws IOException {
public void testForPluginDirFromSysProp() throws IOException {
System.setProperty("cryptomator.pluginDir", pluginDir.toString());
- var cl = ClassLoaderFactory.forPluginDir();
- var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
- var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();
+ try (var cl = ClassLoaderFactory.forPluginDir();
+ var fooIn = cl.getResourceAsStream("foo.properties");
+ var barIn = cl.getResourceAsStream("bar.properties")) {
+ var fooContents = fooIn.readAllBytes();
+ var barContents = barIn.readAllBytes();
- Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
- Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
+ Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
+ Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
+ }
}
}
@@ -126,7 +132,7 @@ public void testFindJars2(@TempDir Path tmpDir) throws IOException {
var urls = ClassLoaderFactory.findJars(tmpDir);
Arrays.sort(urls, Comparator.comparing(URL::toString));
- Assertions.assertArrayEquals(new URL[]{
+ Assertions.assertArrayEquals(new URL[] {
new URL(tmpDir.toUri() + "a.jar"),
new URL(tmpDir.toUri() + "dir2/b.jar")
}, urls);
diff --git a/src/test/java/org/cryptomator/integrations/mount/MountpointTest.java b/src/test/java/org/cryptomator/integrations/mount/MountpointTest.java
new file mode 100644
index 0000000..38bf6c2
--- /dev/null
+++ b/src/test/java/org/cryptomator/integrations/mount/MountpointTest.java
@@ -0,0 +1,55 @@
+package org.cryptomator.integrations.mount;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+public class MountpointTest {
+
+ @Test
+ @DisplayName("MountPoint.forPath()")
+ @DisabledOnOs(OS.WINDOWS)
+ public void testForPath() {
+ var path = Path.of("/foo/bar");
+ var mountPoint = Mountpoint.forPath(path);
+
+ if (mountPoint instanceof Mountpoint.WithPath m) {
+ Assertions.assertEquals(path, m.path());
+ Assertions.assertEquals(URI.create("file:///foo/bar"), mountPoint.uri());
+ } else {
+ Assertions.fail();
+ }
+ }
+
+ @Test
+ @DisplayName("MountPoint.forPath() (Windows)")
+ @EnabledOnOs(OS.WINDOWS)
+ public void testForPathWindows() {
+ var path = Path.of("D:\\foo\\bar");
+ var mountPoint = Mountpoint.forPath(path);
+
+ if (mountPoint instanceof Mountpoint.WithPath m) {
+ Assertions.assertEquals(path, m.path());
+ Assertions.assertEquals(URI.create("file:///D:/foo/bar"), mountPoint.uri());
+ } else {
+ Assertions.fail();
+ }
+ }
+
+ @Test
+ @DisplayName("MountPoint.forUri()")
+ public void testForUri() {
+ var uri = URI.create("webdav://localhost:8080/foo/bar");
+ var mountPoint = Mountpoint.forUri(uri);
+
+ Assertions.assertTrue(mountPoint instanceof Mountpoint.WithUri);
+ Assertions.assertEquals(uri, mountPoint.uri());
+ }
+
+}
\ No newline at end of file