diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed115ca..cf8004f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: - java-version: 17 - distribution: 'temurin' + java-version: 19 + distribution: 'zulu' cache: 'maven' - name: Ensure to use tagged version if: startsWith(github.ref, 'refs/tags/') @@ -25,16 +25,14 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} continue-on-error: true - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: artifacts path: target/*.jar - name: Create Release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') - env: - GITHUB_TOKEN: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} # release as "cryptobot" with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - prerelease: true \ No newline at end of file + prerelease: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ec9182c..0342aac 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,19 +15,19 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: - java-version: 17 - distribution: 'temurin' + java-version: 19 + distribution: 'zulu' cache: 'maven' - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: java - name: Build run: mvn -B compile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 \ No newline at end of file + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index fcbd408..9ad01e7 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -10,13 +10,13 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: "refs/tags/${{ github.event.inputs.tag }}" - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: - java-version: 17 - distribution: 'temurin' + java-version: 19 + distribution: 'zulu' cache: 'maven' server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index ff89e5a..b233f6c 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: - java-version: 17 - distribution: 'temurin' + java-version: 19 + distribution: 'zulu' cache: 'maven' gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase diff --git a/.gitignore b/.gitignore index 8b96cab..7496fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,25 @@ -# Compiled class file *.class -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - # Package Files # *.jar *.war *.ear -*.zip -*.tar.gz -*.rar - -# Maven # -target/ -pom.xml.versionsBackup # Eclipse Settings Files # .settings .project .classpath -test-output/ -# IntelliJ Settings Files # -.idea/ -out/ -.idea_modules/ -*.iml -*.iws +# Maven # +target/ +pom.xml.versionsBackup -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +# IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/compiler.xml +.idea/encodings.xml +.idea/jarRepositories.xml +.idea/**/libraries/ +*.iml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..5ce7f62 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..146ab09 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b385302 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c612358 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/CryptoFS_Mirror.xml b/.idea/runConfigurations/CryptoFS_Mirror.xml new file mode 100644 index 0000000..bb49f9d --- /dev/null +++ b/.idea/runConfigurations/CryptoFS_Mirror.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Mirror.xml b/.idea/runConfigurations/Mirror.xml new file mode 100644 index 0000000..e07961a --- /dev/null +++ b/.idea/runConfigurations/Mirror.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 6b5a3c5..eb93eb4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # fuse-nio-adapter Provides directory contents specified by a `java.nio.file.Path` via a FUSE filesystem. -Uses [jnr-fuse](https://github.com/SerCeMan/jnr-fuse), i.e. you need to install the specified fuse drivers for your OS. +Uses [jfuse](https://github.com/cryptomator/jfuse), i.e. you need to install the specified fuse drivers for your OS. ## Configuration Parameters The following system properties are used: diff --git a/pom.xml b/pom.xml index c62cd04..882e526 100644 --- a/pom.xml +++ b/pom.xml @@ -1,332 +1,363 @@ - - 4.0.0 - org.cryptomator - fuse-nio-adapter - 1.3.4 - FUSE-NIO-Adapter - Access resources at a given NIO path via FUSE. - https://github.com/cryptomator/fuse-nio-adapter + + 4.0.0 + org.cryptomator + fuse-nio-adapter + 2.0.0 + FUSE-NIO-Adapter + Access resources at a given NIO path via FUSE. + https://github.com/cryptomator/fuse-nio-adapter - - scm:git:git@github.com:cryptomator/fuse-nio-adapter.git - scm:git:git@github.com:cryptomator/fuse-nio-adapter.git - git@github.com:cryptomator/fuse-nio-adapter.git - + + scm:git:git@github.com:cryptomator/fuse-nio-adapter.git + scm:git:git@github.com:cryptomator/fuse-nio-adapter.git + git@github.com:cryptomator/fuse-nio-adapter.git + - - UTF-8 - 11 + + UTF-8 + 19 - - 0.5.7 - - 2.39.1 - 30.1.1-jre - 1.7.32 - - - 5.8.2 - 4.5.1 - 2.4.1 - + + 1.2.0 + 0.3.3 + 31.1-jre + 2.0.3 + 3.1.4 - - - GNU Affero General Public License (AGPL) version 3.0 - https://www.gnu.org/licenses/agpl.txt - repo - - + + 5.9.0 + 4.7.0 + 2.4.3 - - - Sebastian Stenzel - sebastian.stenzel@gmail.com - +1 - cryptomator.org - http://cryptomator.org - - + + 8.1.0 + 3.1.0 + - - - - com.github.serceman - jnr-fuse - ${jnrfuse.version} - + + + GNU Affero General Public License (AGPL) version 3.0 + https://www.gnu.org/licenses/agpl.txt + repo + + - - - com.google.dagger - dagger - ${dagger.version} - + + + Sebastian Stenzel + sebastian.stenzel@gmail.com + +1 + cryptomator.org + http://cryptomator.org + + - - - com.google.guava - guava - ${guava.version} - + + + + org.cryptomator + jfuse + ${jfuse.version} + - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-simple - ${slf4j.version} - test - + + + org.cryptomator + integrations-api + ${integrations-api.version} + - - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.jupiter.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-inline - ${mockito.version} - test - - - org.cryptomator - cryptofs - ${cryptofs.version} - test - - + + + com.google.guava + guava + ${guava.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + org.checkerframework + checker-qual + + + com.google.errorprone + error_prone_annotations + + + - - - maven-central - Maven Central Repo - https://repo.maven.apache.org/maven2 - - + + + org.jetbrains + annotations + 23.0.0 + provided + - - - - maven-compiler-plugin - 3.8.1 - - true - - - com.google.dagger - dagger-compiler - ${dagger.version} - - - - - - maven-surefire-plugin - 3.0.0-M5 - - - org.apache.maven.plugins - maven-jar-plugin - 3.2.0 - - - - org.cryptomator.frontend.fuse - - - - - - maven-source-plugin - 3.2.1 - - - attach-sources - - jar-no-fork - - - - - - maven-javadoc-plugin - 3.3.1 - - - attach-javadocs - - jar - - - - - - **/*_* - **/Dagger* - - - - - jakarta.inject - jakarta.inject-api - 1.0.3 - - - - - - - - - - + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + - - - dependency-check - - - - org.owasp - dependency-check-maven - 6.4.1 - - 24 - 0 - true - true - - suppression.xml - - - - - - check - - - - - - - + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-inline + ${mockito.version} + test + + + org.cryptomator + cryptofs + ${cryptofs.version} + test + + - - coverage - - - - org.jacoco - jacoco-maven-plugin - 0.8.7 - - - prepare-agent - - prepare-agent - - - - report - - report - - - - - - - + + + maven-central + Maven Central Repo + https://repo.maven.apache.org/maven2 + + - - sign - - - - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - - + + + + maven-compiler-plugin + 3.10.1 + + true + ${project.build.jdk} + + --enable-preview + + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + prepare-agent + + prepare-agent + + + surefire.jacoco.args + + + + + + maven-surefire-plugin + 3.0.0-M7 + + @{surefire.jacoco.args} --enable-preview --enable-native-access=ALL-UNNAMED + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + maven-javadoc-plugin + 3.4.1 + + + attach-javadocs + + jar + + + ${project.build.jdk} + --enable-preview + + + + + + **/*_* + **/Dagger* + + + + + - - deploy-central - - - ossrh - Maven Central - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - - + + + dependency-check + + + + org.owasp + dependency-check-maven + ${dependency-check.version} + + 24 + 0 + true + true + + suppression.xml + + + + + + check + + + + + + + - - deploy-github - - - github - GitHub Packages - https://maven.pkg.github.com/cryptomator/fuse-nio-adapter - - - - + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + report + + report + + + + + + + + + + sign + + + + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + + deploy-central + + + ossrh + Maven Central + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + + + + + deploy-github + + + github + GitHub Packages + https://maven.pkg.github.com/cryptomator/fuse-nio-adapter + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven.deploy.version} + + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..50a84e8 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,17 @@ +import org.cryptomator.frontend.fuse.mount.FuseTMountProvider; +import org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider; +import org.cryptomator.frontend.fuse.mount.MacFuseMountProvider; +import org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.frontend.fuse.mount.WinFspMountProvider; + +module org.cryptomator.frontend.fuse { + requires org.cryptomator.jfuse; + requires org.cryptomator.integrations.api; + requires org.slf4j; + requires com.google.common; // TODO try to remove + requires com.github.benmanes.caffeine; + requires static org.jetbrains.annotations; + + provides MountService with LinuxFuseMountProvider, MacFuseMountProvider, FuseTMountProvider, WinFspMountProvider, WinFspNetworkMountProvider; +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/frontend/fuse/AdapterFactory.java b/src/main/java/org/cryptomator/frontend/fuse/AdapterFactory.java deleted file mode 100644 index 3ff9d88..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/AdapterFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.nio.file.Path; - -public class AdapterFactory { - - /** - * The default value for the maximum supported filename length. - */ - public static final int DEFAULT_MAX_FILENAMELENGTH = 254; // 255 is preferred, but nautilus checks for this value + 1 - - private AdapterFactory() { - } - - /** - * Creates a read-only fuse-nio filesystem with a maximum file name length of {@value DEFAULT_MAX_FILENAMELENGTH} and an assumed filename encoding of UTF-8 NFC for FUSE and the NIO filesystem. - * @param root the root path of the NIO filesystem. - * @return an adapter mapping FUSE callbacks to the nio interface - * @see ReadOnlyAdapter - * @see FileNameTranscoder - */ - public static FuseNioAdapter createReadOnlyAdapter(Path root) { - return createReadOnlyAdapter(root, DEFAULT_MAX_FILENAMELENGTH, FileNameTranscoder.transcoder() ); - } - - public static FuseNioAdapter createReadOnlyAdapter(Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder) { - FuseNioAdapterComponent comp = DaggerFuseNioAdapterComponent.builder() - .root(root) - .maxFileNameLength(maxFileNameLength) - .fileNameTranscoder(fileNameTranscoder) - .build(); - return comp.readOnlyAdapter(); - } - - /** - * Creates a fuse-nio-filesystem with a maximum file name length of {@value DEFAULT_MAX_FILENAMELENGTH} and an assumed filename encoding of UTF-8 NFC for FUSE and the NIO filesystem. - * @param root the root path of the NIO filesystem. - * @return an adapter mapping FUSE callbacks to the nio interface - * @see ReadWriteAdapter - * @see FileNameTranscoder - */ - public static FuseNioAdapter createReadWriteAdapter(Path root) { - return createReadWriteAdapter(root, DEFAULT_MAX_FILENAMELENGTH); - } - - /** - * Creates a fuse-nio-filesystem with an assumed filename encoding of UTF-8 NFC for FUSE and the NIO filesystem. - * @param root the root path of the NIO filesystem. - * @return an adapter mapping FUSE callbacks to the nio interface - * @see ReadWriteAdapter - * @see FileNameTranscoder - */ - public static FuseNioAdapter createReadWriteAdapter(Path root, int maxFileNameLength) { - return createReadWriteAdapter(root,maxFileNameLength,FileNameTranscoder.transcoder()); - } - - public static FuseNioAdapter createReadWriteAdapter(Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder) { - FuseNioAdapterComponent comp = DaggerFuseNioAdapterComponent.builder().root(root).maxFileNameLength(maxFileNameLength).fileNameTranscoder(fileNameTranscoder).build(); - return comp.readWriteAdapter(); - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/BitMaskEnumUtil.java b/src/main/java/org/cryptomator/frontend/fuse/BitMaskEnumUtil.java deleted file mode 100644 index 20eadf2..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/BitMaskEnumUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.util.EnumSet; -import java.util.Set; - -import javax.inject.Inject; - -import jnr.constants.Constant; - -@PerAdapter -public class BitMaskEnumUtil { - - @Inject - public BitMaskEnumUtil() { - } - - public Set bitMaskToSet(Class clazz, long mask) { - Set result = EnumSet.noneOf(clazz); - for (E e : clazz.getEnumConstants()) { - if ((e.longValue() & mask) == e.longValue()) { - result.add(e); - } - } - return result; - } - - public long setToBitMask(Set set) { - long mask = 0; - for (E value : set) { - mask |= value.longValue(); - } - return mask; - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/FileAttributesUtil.java b/src/main/java/org/cryptomator/frontend/fuse/FileAttributesUtil.java index 144c29b..acde66a 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/FileAttributesUtil.java +++ b/src/main/java/org/cryptomator/frontend/fuse/FileAttributesUtil.java @@ -1,38 +1,34 @@ package org.cryptomator.frontend.fuse; -import jnr.posix.util.Platform; -import ru.serce.jnrfuse.flags.AccessConstants; -import ru.serce.jnrfuse.struct.FileStat; +import org.cryptomator.jfuse.api.Stat; -import javax.inject.Inject; import java.nio.file.AccessMode; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.util.EnumSet; import java.util.Set; -@PerAdapter +@SuppressWarnings("OctalInteger") public class FileAttributesUtil { + // TODO: are default UID/GID system-dependent? // uid/gid are overwritten by fuse mount options -ouid=... private static final int DUMMY_UID = 65534; // usually nobody private static final int DUMMY_GID = 65534; // usually nobody - @Inject - public FileAttributesUtil() { - } + private FileAttributesUtil(){} - public Set accessModeMaskToSet(int mask) { + public static Set accessModeMaskToSet(int mask) { Set accessModes = EnumSet.noneOf(AccessMode.class); // @formatter:off - if ((mask & AccessConstants.R_OK) == AccessConstants.R_OK) accessModes.add(AccessMode.READ); - if ((mask & AccessConstants.W_OK) == AccessConstants.W_OK) accessModes.add(AccessMode.WRITE); - if ((mask & AccessConstants.X_OK) == AccessConstants.X_OK) accessModes.add(AccessMode.EXECUTE); + if ((mask & 4) == 4) accessModes.add(AccessMode.READ); + if ((mask & 2) == 2) accessModes.add(AccessMode.WRITE); + if ((mask & 1) == 1) accessModes.add(AccessMode.EXECUTE); // @formatter:on return accessModes; } - public Set octalModeToPosixPermissions(long mode) { + public static Set octalModeToPosixPermissions(long mode) { Set result = EnumSet.noneOf(PosixFilePermission.class); // @formatter:off if ((mode & 0400) == 0400) result.add(PosixFilePermission.OWNER_READ); @@ -48,49 +44,36 @@ public Set octalModeToPosixPermissions(long mode) { return result; } - public void copyBasicFileAttributesFromNioToFuse(BasicFileAttributes attrs, FileStat stat) { + public static void copyBasicFileAttributesFromNioToFuse(BasicFileAttributes attrs, Stat stat) { + stat.unsetModeBits(Stat.S_IFMT); if (attrs.isDirectory()) { - stat.st_mode.set(stat.st_mode.longValue() | FileStat.S_IFDIR); + stat.setModeBits(Stat.S_IFDIR); } else if (attrs.isRegularFile()) { - stat.st_mode.set(stat.st_mode.longValue() | FileStat.S_IFREG); + stat.setModeBits(Stat.S_IFREG); } else if (attrs.isSymbolicLink()) { - stat.st_mode.set(stat.st_mode.longValue() | FileStat.S_IFLNK); - } - stat.st_uid.set(DUMMY_UID); - stat.st_gid.set(DUMMY_GID); - stat.st_mtim.tv_sec.set(attrs.lastModifiedTime().toInstant().getEpochSecond()); - stat.st_mtim.tv_nsec.set(attrs.lastModifiedTime().toInstant().getNano()); - stat.st_ctim.tv_sec.set(attrs.creationTime().toInstant().getEpochSecond()); - stat.st_ctim.tv_nsec.set(attrs.creationTime().toInstant().getNano()); - if (Platform.IS_MAC || Platform.IS_WINDOWS) { - assert stat.st_birthtime != null; - stat.st_birthtime.tv_sec.set(attrs.creationTime().toInstant().getEpochSecond()); - stat.st_birthtime.tv_nsec.set(attrs.creationTime().toInstant().getNano()); - } - stat.st_atim.tv_sec.set(attrs.lastAccessTime().toInstant().getEpochSecond()); - stat.st_atim.tv_nsec.set(attrs.lastAccessTime().toInstant().getNano()); - stat.st_size.set(attrs.size()); - stat.st_nlink.set(1); - // make sure to nil certain fields known to contain garbage from uninitialized memory - // fixes alleged permission bugs, see https://github.com/cryptomator/fuse-nio-adapter/issues/19 - if (Platform.IS_MAC) { - stat.st_flags.set(0); - stat.st_gen.set(0); + stat.setModeBits(Stat.S_IFLNK); } + stat.setUid(DUMMY_UID); + stat.setGid(DUMMY_GID); + stat.mTime().set(attrs.lastModifiedTime().toInstant()); + stat.birthTime().set(attrs.creationTime().toInstant()); + stat.aTime().set(attrs.lastAccessTime().toInstant()); + stat.setSize(attrs.size()); + stat.setNLink((short) 1); } - public long posixPermissionsToOctalMode(Set permissions) { + public static long posixPermissionsToOctalMode(Set permissions) { long mode = 0; // @formatter:off - if (permissions.contains(PosixFilePermission.OWNER_READ)) mode = mode | FileStat.S_IRUSR; - if (permissions.contains(PosixFilePermission.GROUP_READ)) mode = mode | FileStat.S_IRGRP; - if (permissions.contains(PosixFilePermission.OTHERS_READ)) mode = mode | FileStat.S_IROTH; - if (permissions.contains(PosixFilePermission.OWNER_WRITE)) mode = mode | FileStat.S_IWUSR; - if (permissions.contains(PosixFilePermission.GROUP_WRITE)) mode = mode | FileStat.S_IWGRP; - if (permissions.contains(PosixFilePermission.OTHERS_WRITE)) mode = mode | FileStat.S_IWOTH; - if (permissions.contains(PosixFilePermission.OWNER_EXECUTE)) mode = mode | FileStat.S_IXUSR; - if (permissions.contains(PosixFilePermission.GROUP_EXECUTE)) mode = mode | FileStat.S_IXGRP; - if (permissions.contains(PosixFilePermission.OTHERS_EXECUTE)) mode = mode | FileStat.S_IXOTH; + if (permissions.contains(PosixFilePermission.OWNER_READ)) mode = mode | 0400; + if (permissions.contains(PosixFilePermission.GROUP_READ)) mode = mode | 0040; + if (permissions.contains(PosixFilePermission.OTHERS_READ)) mode = mode | 0004; + if (permissions.contains(PosixFilePermission.OWNER_WRITE)) mode = mode | 0200; + if (permissions.contains(PosixFilePermission.GROUP_WRITE)) mode = mode | 0020; + if (permissions.contains(PosixFilePermission.OTHERS_WRITE)) mode = mode | 0002; + if (permissions.contains(PosixFilePermission.OWNER_EXECUTE)) mode = mode | 0100; + if (permissions.contains(PosixFilePermission.GROUP_EXECUTE)) mode = mode | 0010; + if (permissions.contains(PosixFilePermission.OTHERS_EXECUTE)) mode = mode | 0001; // @formatter:on return mode; } diff --git a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapter.java b/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapter.java index 989881f..f651ee8 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapter.java +++ b/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapter.java @@ -1,12 +1,15 @@ package org.cryptomator.frontend.fuse; -import ru.serce.jnrfuse.FuseFS; +import org.cryptomator.jfuse.api.FuseOperations; -import java.util.concurrent.TimeoutException; +import java.io.IOException; -public interface FuseNioAdapter extends FuseFS, AutoCloseable { +public interface FuseNioAdapter extends FuseOperations, AutoCloseable { - boolean isMounted(); + /** + * The default value for the maximum supported filename length. + */ + int DEFAULT_MAX_FILENAMELENGTH = 254; // 255 is preferred, but nautilus checks for this value + 1 /** * Checks if the filesystem is in use (and therefore an unmount attempt should be avoided). @@ -20,20 +23,6 @@ public interface FuseNioAdapter extends FuseFS, AutoCloseable { */ boolean isInUse(); - /** - * Sets mounted to false. - *

- * Allows custom unmount implementations to prevent subsequent invocations of {@link #umount()} to run into illegal states. - */ - void setUnmounted(); - - /** - * If the init() callback of fuse_operations is implemented, this method blocks until it is called or a specified timeout is hit. Otherwise returns directly. - * - * @param timeOutMillis the timeout in milliseconds to wait until the init() call - * @throws InterruptedException If the waiting thread is interrupted. - * @throws TimeoutException If the waiting thread waits longer than the specified {@code timeout}. - */ - void awaitInitCall(long timeOutMillis) throws InterruptedException, TimeoutException; - + @Override + void close() throws IOException; } diff --git a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterComponent.java b/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterComponent.java deleted file mode 100644 index ee4a1dd..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterComponent.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import dagger.BindsInstance; -import dagger.Component; - -import javax.inject.Named; -import java.nio.file.Path; - -@PerAdapter -@Component(modules = {FuseNioAdapterModule.class}) -public interface FuseNioAdapterComponent { - - ReadOnlyAdapter readOnlyAdapter(); - - ReadWriteAdapter readWriteAdapter(); - - @Component.Builder - interface Builder { - - @BindsInstance - Builder root(@Named("root") Path root); - - @BindsInstance - Builder maxFileNameLength(@Named("maxFileNameLength") int maxFileNameLength); - - @BindsInstance - Builder fileNameTranscoder(FileNameTranscoder fileNameTranscoder); - - FuseNioAdapterComponent build(); - } - - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterModule.java b/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterModule.java deleted file mode 100644 index 1887645..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/FuseNioAdapterModule.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.FileStore; -import java.nio.file.Files; -import java.nio.file.Path; - -import javax.inject.Named; - -import dagger.Module; -import dagger.Provides; - -@Module -class FuseNioAdapterModule { - - @Provides - @PerAdapter - protected FileStore provideRootFileStore(@Named("root") Path root) { - try { - return Files.getFileStore(root); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/OpenFile.java b/src/main/java/org/cryptomator/frontend/fuse/OpenFile.java index 84c645b..381b7d6 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/OpenFile.java +++ b/src/main/java/org/cryptomator/frontend/fuse/OpenFile.java @@ -1,5 +1,9 @@ package org.cryptomator.frontend.fuse; +import com.google.common.base.MoreObjects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -9,11 +13,6 @@ import java.nio.file.attribute.FileAttribute; import java.util.Set; -import com.google.common.base.MoreObjects; -import jnr.ffi.Pointer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class OpenFile implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(OpenFile.class); @@ -41,28 +40,24 @@ static OpenFile create(Path path, Set options, FileAttribu * @return Actual number of bytes read (can be less than {@code size} if reached EOF). * @throws IOException If an exception occurs during read. */ - public synchronized int read(Pointer buf, long num, long offset) throws IOException { + public int read(ByteBuffer buf, long num, long offset) throws IOException { long size = channel.size(); if (offset >= size) { return 0; + } else if (num > Integer.MAX_VALUE) { + throw new IOException("Requested too many bytes"); } else { - ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE); - long pos = 0; - channel.position(offset); - LOG.trace("Attempting to read {}-{}:", offset, offset + num); - do { - long remaining = num - pos; - int read = readNext(bb, remaining); - if (read == -1) { + int read = 0; + int toRead = (int) Math.min(num, buf.limit()); + while (read < toRead) { + int r = channel.read(buf, offset + read); + if (r == -1) { LOG.trace("Reached EOF"); - return (int) pos; // reached EOF TODO: wtf cast - } else { - LOG.trace("Reading {}-{}", offset + pos, offset + pos + read); - buf.put(pos, bb.array(), 0, read); - pos += read; + break; } - } while (pos < num); - return (int) pos; // TODO wtf cast + read += r; + } + return read; } } @@ -73,29 +68,18 @@ public synchronized int read(Pointer buf, long num, long offset) throws IOExcept * @param num Number of bytes to write * @param offset Position of first byte to write at * @return Actual number of bytes written - * TODO: only the bytes which contains information or also some filling zeros? * @throws IOException If an exception occurs during write. */ - public synchronized int write(Pointer buf, long num, long offset) throws IOException { - ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE); - long written = 0; - channel.position(offset); - do { - long remaining = num - written; - bb.clear(); - int len = (int) Math.min(remaining, bb.capacity()); - buf.get(written, bb.array(), 0, len); - bb.limit(len); - channel.write(bb); // TODO check return value - written += len; - } while (written < num); - return (int) written; // TODO wtf cast - } - - private int readNext(ByteBuffer readBuf, long num) throws IOException { - readBuf.clear(); - readBuf.limit((int) Math.min(readBuf.capacity(), num)); - return channel.read(readBuf); + public int write(ByteBuffer buf, long num, long offset) throws IOException { + if (num > Integer.MAX_VALUE) { + throw new IOException("Requested too many bytes"); + } + int written = 0; + int toWrite = (int) Math.min(num, buf.limit()); + while (written < toWrite) { + written += channel.write(buf, offset + written); + } + return written; } @Override diff --git a/src/main/java/org/cryptomator/frontend/fuse/OpenFileFactory.java b/src/main/java/org/cryptomator/frontend/fuse/OpenFileFactory.java index 4747503..c223fca 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/OpenFileFactory.java +++ b/src/main/java/org/cryptomator/frontend/fuse/OpenFileFactory.java @@ -1,14 +1,14 @@ package org.cryptomator.frontend.fuse; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.channels.ClosedChannelException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; -import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Set; @@ -16,13 +16,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; -import javax.inject.Inject; - -import com.google.common.collect.Sets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@PerAdapter public class OpenFileFactory implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(OpenFileFactory.class); @@ -30,10 +23,6 @@ public class OpenFileFactory implements AutoCloseable { private final ConcurrentMap openFiles = new ConcurrentHashMap<>(); private final AtomicLong fileHandleGen = new AtomicLong(1l); - @Inject - public OpenFileFactory() { - } - /** * @param path path of the file to open * @param options file open options @@ -94,8 +83,8 @@ public int getOpenFileCount(){ @Override public synchronized void close() throws IOException { IOException exception = new IOException("At least one open file could not be closed."); - for (Iterator> it = openFiles.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); + for (var it = openFiles.entrySet().iterator(); it.hasNext();) { + var entry = it.next(); OpenFile openFile = entry.getValue(); LOG.warn("Closing unclosed file {}", openFile); try { diff --git a/src/main/java/org/cryptomator/frontend/fuse/OpenOptionsUtil.java b/src/main/java/org/cryptomator/frontend/fuse/OpenOptionsUtil.java deleted file mode 100644 index 5af40c4..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/OpenOptionsUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.nio.file.OpenOption; -import java.nio.file.StandardOpenOption; -import java.util.HashSet; -import java.util.Set; - -import javax.inject.Inject; - -import jnr.constants.platform.OpenFlags; - -@PerAdapter -public class OpenOptionsUtil { - - private final BitMaskEnumUtil bitMaskUtil; - - @Inject - public OpenOptionsUtil(BitMaskEnumUtil bitMaskUtil) { - this.bitMaskUtil = bitMaskUtil; - } - - public Set fuseOpenFlagsToNioOpenOptions(long mask) { - Set flags = bitMaskUtil.bitMaskToSet(OpenFlags.class, mask); - return fuseOpenFlagsToNioOpenOptions(flags); - } - - public Set fuseOpenFlagsToNioOpenOptions(Set flags) { - Set result = new HashSet<>(); - // https://linux.die.net/man/3/open: - if (flags.contains(OpenFlags.O_RDWR)) { - result.add(StandardOpenOption.READ); - result.add(StandardOpenOption.WRITE); - } else if (flags.contains(OpenFlags.O_WRONLY)) { - result.add(StandardOpenOption.WRITE); - } else if (flags.contains(OpenFlags.O_RDONLY)) { - result.add(StandardOpenOption.READ); - } - if (flags.contains(OpenFlags.O_APPEND)) { - result.add(StandardOpenOption.APPEND); - } - if (flags.contains(OpenFlags.O_TRUNC)) { - result.add(StandardOpenOption.TRUNCATE_EXISTING); - } - return result; - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/PerAdapter.java b/src/main/java/org/cryptomator/frontend/fuse/PerAdapter.java deleted file mode 100644 index 3d27e6a..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/PerAdapter.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -import javax.inject.Scope; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Scope -@Documented -@Retention(RUNTIME) -public @interface PerAdapter { -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyAdapter.java b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyAdapter.java index 873f278..3dc7b52 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyAdapter.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyAdapter.java @@ -3,25 +3,21 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import com.google.common.collect.Iterables; -import jnr.ffi.Pointer; -import jnr.ffi.types.off_t; -import jnr.ffi.types.size_t; import org.cryptomator.frontend.fuse.locks.AlreadyLockedException; import org.cryptomator.frontend.fuse.locks.DataLock; import org.cryptomator.frontend.fuse.locks.LockManager; import org.cryptomator.frontend.fuse.locks.PathLock; +import org.cryptomator.jfuse.api.DirFiller; +import org.cryptomator.jfuse.api.Errno; +import org.cryptomator.jfuse.api.FileInfo; +import org.cryptomator.jfuse.api.Stat; +import org.cryptomator.jfuse.api.Statvfs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ru.serce.jnrfuse.ErrorCodes; -import ru.serce.jnrfuse.FuseFillDir; -import ru.serce.jnrfuse.FuseStubFS; -import ru.serce.jnrfuse.struct.FileStat; -import ru.serce.jnrfuse.struct.FuseFileInfo; -import ru.serce.jnrfuse.struct.Statvfs; -import javax.inject.Inject; -import javax.inject.Named; import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; @@ -39,44 +35,71 @@ import java.util.Collections; import java.util.EnumSet; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.function.BooleanSupplier; -/** - * Read-Only FUSE-NIO-Adapter based on Sergey Tselovalnikov's HelloFuse - */ -@PerAdapter -public class ReadOnlyAdapter extends FuseStubFS implements FuseNioAdapter { +public sealed class ReadOnlyAdapter implements FuseNioAdapter permits ReadWriteAdapter { private static final Logger LOG = LoggerFactory.getLogger(ReadOnlyAdapter.class); private static final int BLOCKSIZE = 4096; + protected final Errno errno; protected final Path root; private final int maxFileNameLength; protected final FileStore fileStore; protected final LockManager lockManager; + protected final OpenFileFactory openFiles; protected final FileNameTranscoder fileNameTranscoder; private final ReadOnlyDirectoryHandler dirHandler; private final ReadOnlyFileHandler fileHandler; private final ReadOnlyLinkHandler linkHandler; - private final FileAttributesUtil attrUtil; private final BooleanSupplier hasOpenFiles; - private final CountDownLatch initSignaler; - @Inject - public ReadOnlyAdapter(@Named("root") Path root, @Named("maxFileNameLength") int maxFileNameLength, FileNameTranscoder fileNameTranscoder, FileStore fileStore, LockManager lockManager, ReadOnlyDirectoryHandler dirHandler, ReadOnlyFileHandler fileHandler, ReadOnlyLinkHandler linkHandler, FileAttributesUtil attrUtil, OpenFileFactory fileFactory) { + protected ReadOnlyAdapter(Errno errno, Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder, FileStore fileStore, OpenFileFactory openFiles, ReadOnlyDirectoryHandler dirHandler, ReadOnlyFileHandler fileHandler) { + this.errno = errno; this.root = root; this.maxFileNameLength = maxFileNameLength; this.fileNameTranscoder = fileNameTranscoder; this.fileStore = fileStore; - this.lockManager = lockManager; + this.lockManager = new LockManager(); + this.openFiles = openFiles; this.dirHandler = dirHandler; this.fileHandler = fileHandler; - this.linkHandler = linkHandler; - this.attrUtil = attrUtil; - this.hasOpenFiles = () -> fileFactory.getOpenFileCount() != 0; - this.initSignaler = new CountDownLatch(1); + this.linkHandler = new ReadOnlyLinkHandler(fileNameTranscoder); + this.hasOpenFiles = () -> openFiles.getOpenFileCount() != 0; + } + + public static ReadOnlyAdapter create(Errno errno, Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder) { + try { + var fileStore = Files.getFileStore(root); + var openFiles = new OpenFileFactory(); + var dirHandler = new ReadOnlyDirectoryHandler(fileNameTranscoder); + var fileHandler = new ReadOnlyFileHandler(openFiles); + return new ReadOnlyAdapter(errno, root, maxFileNameLength, fileNameTranscoder, fileStore, openFiles, dirHandler, fileHandler); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public Errno errno() { + return errno; + } + + @Override + public Set supportedOperations() { + return Set.of(Operation.ACCESS, + Operation.CHMOD, + Operation.CREATE, + Operation.DESTROY, + Operation.GET_ATTR, + Operation.INIT, + Operation.OPEN, + Operation.OPEN_DIR, + Operation.READ, + Operation.READLINK, + Operation.READ_DIR, + Operation.RELEASE, + Operation.RELEASE_DIR, + Operation.STATFS); } protected Path resolvePath(String absolutePath) { @@ -91,17 +114,17 @@ public int statfs(String path, Statvfs stbuf) { long avail = fileStore.getUsableSpace(); long tBlocks = total / BLOCKSIZE; long aBlocks = avail / BLOCKSIZE; - stbuf.f_bsize.set(BLOCKSIZE); - stbuf.f_frsize.set(BLOCKSIZE); - stbuf.f_blocks.set(tBlocks); - stbuf.f_bavail.set(aBlocks); - stbuf.f_bfree.set(aBlocks); - stbuf.f_namemax.set(maxFileNameLength); + stbuf.setBsize(BLOCKSIZE); + stbuf.setFrsize(BLOCKSIZE); + stbuf.setBlocks(tBlocks); + stbuf.setBavail(aBlocks); + stbuf.setBfree(aBlocks); + stbuf.setNameMax(maxFileNameLength); LOG.trace("statfs {} ({} / {})", path, avail, total); return 0; } catch (IOException | RuntimeException e) { LOG.error("statfs " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @@ -109,11 +132,11 @@ public int statfs(String path, Statvfs stbuf) { public int access(String path, int mask) { try { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); - Set accessModes = attrUtil.accessModeMaskToSet(mask); + Set accessModes = FileAttributesUtil.accessModeMaskToSet(mask); return checkAccess(node, accessModes); } catch (RuntimeException e) { LOG.error("checkAccess failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @@ -129,33 +152,33 @@ protected int checkAccess(Path path, Set requiredAccessModes, Set() { @@ -77,9 +66,7 @@ public int readdir(Path path, Pointer buf, FuseFillDir filler, long offset, Fuse Iterator iter = Iterators.concat(sameAndParent, ds.iterator()); while (iter.hasNext()) { String fileName = iter.next().getFileName().toString(); - if (filler.apply(buf, fileNameTranscoder.nioToFuse(fileName), null, 0) != 0) { - return -ErrorCodes.ENOMEM(); - } + filler.fill(fileNameTranscoder.nioToFuse(fileName)); } return 0; } catch (DirectoryIteratorException e) { diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyFileHandler.java b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyFileHandler.java index 7346b5a..435d93a 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyFileHandler.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyFileHandler.java @@ -1,39 +1,31 @@ package org.cryptomator.frontend.fuse; -import jnr.ffi.Pointer; -import ru.serce.jnrfuse.struct.FileStat; -import ru.serce.jnrfuse.struct.FuseFileInfo; +import org.cryptomator.jfuse.api.FileInfo; +import org.cryptomator.jfuse.api.Stat; -import javax.inject.Inject; import java.io.Closeable; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.file.AccessDeniedException; -import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributes; import java.util.Set; -@PerAdapter public class ReadOnlyFileHandler implements Closeable { protected final OpenFileFactory openFiles; - protected final FileAttributesUtil attrUtil; - private final OpenOptionsUtil openOptionsUtil; - @Inject - public ReadOnlyFileHandler(OpenFileFactory openFiles, FileAttributesUtil attrUtil, OpenOptionsUtil openOptionsUtil) { + public ReadOnlyFileHandler(OpenFileFactory openFiles) { this.openFiles = openFiles; - this.attrUtil = attrUtil; - this.openOptionsUtil = openOptionsUtil; } - public void open(Path path, FuseFileInfo fi) throws IOException { - Set openOptions = openOptionsUtil.fuseOpenFlagsToNioOpenOptions(fi.flags.longValue()); + public void open(Path path, FileInfo fi) throws IOException { + var openOptions = fi.getOpenFlags(); long fileHandle = open(path, openOptions); - fi.fh.set(fileHandle); + fi.setFh(fileHandle); } /** @@ -43,7 +35,7 @@ public void open(Path path, FuseFileInfo fi) throws IOException { * @throws AccessDeniedException Thrown if the requested openOptions are not supported * @throws IOException */ - protected long open(Path path, Set openOptions) throws AccessDeniedException, IOException { + protected long open(Path path, Set openOptions) throws AccessDeniedException, IOException { if (openOptions.contains(StandardOpenOption.WRITE)) { throw new AccessDeniedException(path.toString(), null, "Unsupported open options: WRITE"); } else { @@ -62,8 +54,8 @@ protected long open(Path path, Set openOptions) throws AccessDeniedE * @throws ClosedChannelException If no open file could be found for the given file handle * @throws IOException */ - public int read(Pointer buf, long size, long offset, FuseFileInfo fi) throws IOException { - OpenFile file = openFiles.get(fi.fh.get()); + public int read(ByteBuffer buf, long size, long offset, FileInfo fi) throws IOException { + OpenFile file = openFiles.get(fi.getFh()); if (file == null) { throw new ClosedChannelException(); } @@ -77,20 +69,17 @@ public int read(Pointer buf, long size, long offset, FuseFileInfo fi) throws IOE * @throws ClosedChannelException If no channel for the given fileHandle has been found. * @throws IOException */ - public void release(FuseFileInfo fi) throws IOException { - openFiles.close(fi.fh.get()); + public void release(FileInfo fi) throws IOException { + openFiles.close(fi.getFh()); } - public int getattr(Path node, BasicFileAttributes attrs, FileStat stat) { - if (attrs instanceof PosixFileAttributes) { - PosixFileAttributes posixAttrs = (PosixFileAttributes) attrs; - long mode = attrUtil.posixPermissionsToOctalMode(posixAttrs.permissions()); - mode = mode & 0555; - stat.st_mode.set(FileStat.S_IFREG | mode); + public int getattr(Path node, BasicFileAttributes attrs, Stat stat) { + if (attrs instanceof PosixFileAttributes posixAttrs) { + stat.setPermissions(posixAttrs.permissions()); } else { - stat.st_mode.set(FileStat.S_IFREG | 0444); + stat.setMode(0555); } - attrUtil.copyBasicFileAttributesFromNioToFuse(attrs, stat); + FileAttributesUtil.copyBasicFileAttributesFromNioToFuse(attrs, stat); return 0; } diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyLinkHandler.java b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyLinkHandler.java index 190206c..85242f8 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyLinkHandler.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadOnlyLinkHandler.java @@ -1,42 +1,33 @@ package org.cryptomator.frontend.fuse; -import jnr.ffi.Pointer; +import org.cryptomator.jfuse.api.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ru.serce.jnrfuse.struct.FileStat; -import javax.inject.Inject; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; +import java.nio.file.NotLinkException; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributes; -@PerAdapter class ReadOnlyLinkHandler { - private static final Logger LOG = LoggerFactory.getLogger(ReadOnlyLinkHandler.class); - - private final FileAttributesUtil attrUtil; private final FileNameTranscoder fileNameTranscoder; - @Inject - public ReadOnlyLinkHandler(FileAttributesUtil attrUtil, FileNameTranscoder fileNameTranscoder) { - this.attrUtil = attrUtil; + public ReadOnlyLinkHandler(FileNameTranscoder fileNameTranscoder) { this.fileNameTranscoder = fileNameTranscoder; } - public int getattr(Path path, BasicFileAttributes attrs, FileStat stat) { - if (attrs instanceof PosixFileAttributes) { - PosixFileAttributes posixAttrs = (PosixFileAttributes) attrs; - long mode = attrUtil.posixPermissionsToOctalMode(posixAttrs.permissions()); - mode = mode & 0555; - stat.st_mode.set(FileStat.S_IFLNK | mode); + public int getattr(Path path, BasicFileAttributes attrs, Stat stat) { + if (attrs instanceof PosixFileAttributes posixAttrs) { + stat.setPermissions(posixAttrs.permissions()); } else { - stat.st_mode.set(FileStat.S_IFLNK | 0555); + stat.setMode(0555); } - attrUtil.copyBasicFileAttributesFromNioToFuse(attrs, stat); + stat.setModeBits(Stat.S_IFLNK); + FileAttributesUtil.copyBasicFileAttributesFromNioToFuse(attrs, stat); return 0; } @@ -48,13 +39,16 @@ public int getattr(Path path, BasicFileAttributes attrs, FileStat stat) { * @return * @throws IOException */ - public int readlink(Path path, Pointer buf, long size) throws IOException { + public int readlink(Path path, ByteBuffer buf, long size) throws IOException { + if(path.getParent() == null) { + throw new NotLinkException("Root cannot be a link"); + } Path target = Files.readSymbolicLink(path); ByteBuffer fuseEncodedTarget = fileNameTranscoder.interpretAsFuseString(fileNameTranscoder.nioToFuse(target.toString())); int len = (int) Math.min(fuseEncodedTarget.remaining(), size - 1); assert len < size; buf.put(0, fuseEncodedTarget.array(), 0, len); - buf.putByte(len, (byte) 0x00); // add null terminator + buf.put(len, (byte) 0x00); // add null terminator return 0; } diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteAdapter.java b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteAdapter.java index 8190aa6..98cc96a 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteAdapter.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteAdapter.java @@ -1,24 +1,16 @@ package org.cryptomator.frontend.fuse; -import jnr.constants.platform.OpenFlags; -import jnr.ffi.Pointer; -import jnr.ffi.types.gid_t; -import jnr.ffi.types.mode_t; -import jnr.ffi.types.off_t; -import jnr.ffi.types.size_t; -import jnr.ffi.types.uid_t; import org.cryptomator.frontend.fuse.locks.DataLock; -import org.cryptomator.frontend.fuse.locks.LockManager; import org.cryptomator.frontend.fuse.locks.PathLock; +import org.cryptomator.jfuse.api.Errno; +import org.cryptomator.jfuse.api.FileInfo; +import org.cryptomator.jfuse.api.TimeSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ru.serce.jnrfuse.ErrorCodes; -import ru.serce.jnrfuse.struct.FuseFileInfo; -import ru.serce.jnrfuse.struct.Timespec; -import javax.inject.Inject; -import javax.inject.Named; import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.file.AccessMode; import java.nio.file.DirectoryNotEmptyException; @@ -35,27 +27,50 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermissions; -import java.time.DateTimeException; import java.util.EnumSet; import java.util.Set; /** * */ -@PerAdapter -public class ReadWriteAdapter extends ReadOnlyAdapter { +public final class ReadWriteAdapter extends ReadOnlyAdapter { private static final Logger LOG = LoggerFactory.getLogger(ReadWriteAdapter.class); private final ReadWriteFileHandler fileHandler; - private final FileAttributesUtil attrUtil; - private final BitMaskEnumUtil bitMaskUtil; - @Inject - public ReadWriteAdapter(@Named("root") Path root, @Named("maxFileNameLength") int maxFileNameLength, FileNameTranscoder fileNameTranscoder, FileStore fileStore, LockManager lockManager, ReadWriteDirectoryHandler dirHandler, ReadWriteFileHandler fileHandler, ReadOnlyLinkHandler linkHandler, FileAttributesUtil attrUtil, BitMaskEnumUtil bitMaskUtil, OpenFileFactory fileFactory) { - super(root, maxFileNameLength, fileNameTranscoder, fileStore, lockManager, dirHandler, fileHandler, linkHandler, attrUtil, fileFactory); + private ReadWriteAdapter(Errno errno, Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder, FileStore fileStore, OpenFileFactory openFiles, ReadWriteDirectoryHandler dirHandler, ReadWriteFileHandler fileHandler) { + super(errno, root, maxFileNameLength, fileNameTranscoder, fileStore, openFiles, dirHandler, fileHandler); this.fileHandler = fileHandler; - this.attrUtil = attrUtil; - this.bitMaskUtil = bitMaskUtil; + } + + public static ReadWriteAdapter create(Errno errno, Path root, int maxFileNameLength, FileNameTranscoder fileNameTranscoder) { + try { + var fileStore = Files.getFileStore(root); + var openFiles = new OpenFileFactory(); + var dirHandler = new ReadWriteDirectoryHandler(fileNameTranscoder); + var fileHandler = new ReadWriteFileHandler(openFiles); + return new ReadWriteAdapter(errno, root, maxFileNameLength, fileNameTranscoder, fileStore, openFiles, dirHandler, fileHandler); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public Set supportedOperations() { + var ops = EnumSet.copyOf(super.supportedOperations()); + ops.add(Operation.CHMOD); + //ops.add(Operation.CHOWN); + ops.add(Operation.CREATE); + //ops.add(Operation.FSYNC); + ops.add(Operation.MKDIR); + ops.add(Operation.RENAME); + ops.add(Operation.RMDIR); + ops.add(Operation.SYMLINK); + ops.add(Operation.TRUNCATE); + ops.add(Operation.UNLINK); + ops.add(Operation.UTIMENS); + ops.add(Operation.WRITE); + return ops; } @Override @@ -64,8 +79,8 @@ protected int checkAccess(Path path, Set requiredAccessModes) { } @Override - public int mkdir(String path, @mode_t long mode) { - try (PathLock pathLock = lockManager.createPathLock(path).forWriting(); + public int mkdir(String path, int mode) { + try (PathLock pathLock = lockManager.lockForWriting(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); LOG.trace("mkdir {} ({})", path, mode); @@ -73,18 +88,18 @@ public int mkdir(String path, @mode_t long mode) { return 0; } catch (FileAlreadyExistsException e) { LOG.warn("mkdir {} failed, file already exists.", path); - return -ErrorCodes.EEXIST(); + return -errno.eexist(); } catch (FileSystemException e) { return getErrorCodeForGenericFileSystemException(e, "mkdir " + path); } catch (IOException | RuntimeException e) { LOG.error("mkdir " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override public int symlink(String targetPath, String linkPath) { - try (PathLock pathLock = lockManager.createPathLock(linkPath).forWriting(); + try (PathLock pathLock = lockManager.lockForWriting(linkPath); DataLock dataLock = pathLock.lockDataForWriting()) { Path link = resolvePath(fileNameTranscoder.fuseToNio(linkPath)); Path target = link.getFileSystem().getPath(fileNameTranscoder.fuseToNio(targetPath)); @@ -93,24 +108,24 @@ public int symlink(String targetPath, String linkPath) { return 0; } catch (FileAlreadyExistsException e) { LOG.warn("symlink {} -> {} failed, file already exists.", linkPath, targetPath); - return -ErrorCodes.EEXIST(); + return -errno.eexist(); } catch (FileSystemException e) { return getErrorCodeForGenericFileSystemException(e, "symlink " + targetPath + " -> " + linkPath); } catch (IOException | RuntimeException e) { LOG.error("symlink failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int create(String path, @mode_t long mode, FuseFileInfo fi) { - try (PathLock pathLock = lockManager.createPathLock(path).forWriting(); + public int create(String path, int mode, FileInfo fi) { + try (PathLock pathLock = lockManager.lockForWriting(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); - Set flags = bitMaskUtil.bitMaskToSet(OpenFlags.class, fi.flags.longValue()); + var flags = fi.getOpenFlags(); LOG.trace("create {} with flags {}", path, flags); if (fileStore.supportsFileAttributeView(PosixFileAttributeView.class)) { - FileAttribute attrs = PosixFilePermissions.asFileAttribute(attrUtil.octalModeToPosixPermissions(mode)); + FileAttribute attrs = PosixFilePermissions.asFileAttribute(FileAttributesUtil.octalModeToPosixPermissions(mode)); fileHandler.createAndOpen(node, fi, attrs); } else { fileHandler.createAndOpen(node, fi); @@ -118,65 +133,65 @@ public int create(String path, @mode_t long mode, FuseFileInfo fi) { return 0; } catch (FileAlreadyExistsException e) { LOG.warn("create {} failed, file already exists.", path); - return -ErrorCodes.EEXIST(); + return -errno.eexist(); } catch (FileSystemException e) { return getErrorCodeForGenericFileSystemException(e, "create " + path); } catch (IOException | RuntimeException e) { LOG.error("create " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int chown(String path, @uid_t long uid, @gid_t long gid) { + public int chown(String path, int uid, int gid, FileInfo fi) { LOG.trace("Ignoring chown(uid={}, gid={}) call. Files will be served with static uid/gid.", uid, gid); return 0; } @Override - public int chmod(String path, @mode_t long mode) { - try (PathLock pathLock = lockManager.createPathLock(path).forReading(); + public int chmod(String path, int mode, FileInfo fi) { + try (PathLock pathLock = lockManager.lockForReading(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); LOG.trace("chmod {} ({})", path, mode); - Files.setPosixFilePermissions(node, attrUtil.octalModeToPosixPermissions(mode)); + Files.setPosixFilePermissions(node, FileAttributesUtil.octalModeToPosixPermissions(mode)); return 0; } catch (NoSuchFileException e) { LOG.warn("chmod {} failed, file not found.", path); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (UnsupportedOperationException e) { LOG.warn("Setting posix permissions not supported by underlying file system."); - return -ErrorCodes.ENOSYS(); + return -errno.enosys(); } catch (IOException | RuntimeException e) { LOG.error("chmod " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override public int unlink(String path) { - try (PathLock pathLock = lockManager.createPathLock(path).forWriting(); + try (PathLock pathLock = lockManager.lockForWriting(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); if (Files.isDirectory(node, LinkOption.NOFOLLOW_LINKS)) { LOG.warn("unlink {} failed, node is a directory.", path); - return -ErrorCodes.EISDIR(); + return -errno.eisdir(); } LOG.trace("unlink {}", path); Files.delete(node); return 0; } catch (NoSuchFileException e) { LOG.warn("unlink {} failed, file not found.", path); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (IOException | RuntimeException e) { LOG.error("unlink " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override public int rmdir(String path) { - try (PathLock pathLock = lockManager.createPathLock(path).forWriting(); + try (PathLock pathLock = lockManager.lockForWriting(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); if (!Files.isDirectory(node, LinkOption.NOFOLLOW_LINKS)) { @@ -189,21 +204,21 @@ public int rmdir(String path) { return 0; } catch (NotDirectoryException e) { LOG.warn("rmdir {} failed, node is not a directory.", path); - return -ErrorCodes.ENOTDIR(); + return -errno.enotdir(); } catch (NoSuchFileException e) { LOG.warn("rmdir {} failed, file not found.", path); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (DirectoryNotEmptyException e) { LOG.warn("rmdir {} failed, directory not empty.", path); - return -ErrorCodes.ENOTEMPTY(); + return -errno.enotempty(); } catch (IOException | RuntimeException e) { LOG.error("rmdir " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } /** - * Specialised method on MacOS due to the usage of the -noappledouble option in the {@link org.cryptomator.frontend.fuse.mount.MacMounter} and the possible existence of AppleDouble or DSStore-Files. + * Specialised method on MacOS due to the usage of the -noappledouble option in the {@link org.cryptomator.frontend.fuse.mount.MacFuseMountProvider} and the possible existence of AppleDouble or DSStore-Files. * * @param node the directory path for which is checked for such files * @throws IOException if an AppleDouble file cannot be deleted or opening of a directory stream fails @@ -218,10 +233,10 @@ private void deleteAppleDoubleFiles(Path node) throws IOException { } @Override - public int rename(String oldPath, String newPath) { - try (PathLock oldPathLock = lockManager.createPathLock(oldPath).forWriting(); + public int rename(String oldPath, String newPath, int flags) { + try (PathLock oldPathLock = lockManager.lockForWriting(oldPath); DataLock oldDataLock = oldPathLock.lockDataForWriting(); - PathLock newPathLock = lockManager.createPathLock(newPath).forWriting(); + PathLock newPathLock = lockManager.lockForWriting(newPath); DataLock newDataLock = newPathLock.lockDataForWriting()) { // TODO: recursively check for open file handles Path nodeOld = resolvePath(fileNameTranscoder.fuseToNio(oldPath)); @@ -231,107 +246,86 @@ public int rename(String oldPath, String newPath) { return 0; } catch (NoSuchFileException e) { LOG.warn("rename {} to {} failed, file not found.", oldPath, newPath); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (DirectoryNotEmptyException e) { LOG.warn("rename {} to {} failed, directory not empty.", oldPath, newPath); - return -ErrorCodes.ENOTEMPTY(); + return -errno.enotempty(); } catch (FileSystemException e) { return getErrorCodeForGenericFileSystemException(e, "rename " + oldPath + " -> " + newPath); } catch (IOException | RuntimeException e) { LOG.error("rename " + oldPath + " to " + newPath + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int utimens(String path, Timespec[] timespec) { - /* - * From utimensat(2) man page: - * the array times: times[0] specifies the new "last access time" (atime); - * times[1] specifies the new "last modification time" (mtime). - */ - assert timespec.length == 2; - try (PathLock pathLock = lockManager.createPathLock(path).forReading(); + public int utimens(String path, TimeSpec atime, TimeSpec mtime, FileInfo fi) { + try (PathLock pathLock = lockManager.lockForReading(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); - LOG.trace("utimens {} (last modification {} sec {} nsec, last access {} sec {} nsec)", path, timespec[1].tv_sec.get(), timespec[1].tv_nsec.longValue(), timespec[0].tv_sec.get(), timespec[0].tv_nsec.longValue()); - fileHandler.utimens(node, timespec[1], timespec[0]); + LOG.trace("utimens {} (last modification {}, last access {})", path, mtime, atime); + fileHandler.utimens(node, mtime, atime); return 0; - } catch (DateTimeException | ArithmeticException e) { - LOG.warn("utimens {} failed, invalid argument.", e); - return -ErrorCodes.EINVAL(); } catch (NoSuchFileException e) { LOG.warn("utimens {} failed, file not found.", path); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (IOException | RuntimeException e) { LOG.error("utimens " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int write(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) { - try (PathLock pathLock = lockManager.createPathLock(path).forReading(); + public int write(String path, ByteBuffer buf, long size, long offset, FileInfo fi) { + try (PathLock pathLock = lockManager.lockForReading(path); DataLock dataLock = pathLock.lockDataForWriting()) { LOG.trace("write {} bytes to file {} starting at {}...", size, path, offset); int written = fileHandler.write(buf, size, offset, fi); LOG.trace("wrote {} bytes to file {}.", written, path); return written; } catch (ClosedChannelException e) { - LOG.warn("write {} failed, invalid file handle {}", path, fi.fh.get()); - return -ErrorCodes.EBADF(); + LOG.warn("write {} failed, invalid file handle {}", path, fi.getFh()); + return -errno.ebadf(); } catch (IOException | RuntimeException e) { LOG.error("write " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int truncate(String path, @off_t long size) { - try (PathLock pathLock = lockManager.createPathLock(path).forReading(); + public int truncate(String path, long size, FileInfo fi) { + try (PathLock pathLock = lockManager.lockForReading(path); DataLock dataLock = pathLock.lockDataForWriting()) { Path node = resolvePath(fileNameTranscoder.fuseToNio(path)); LOG.trace("truncate {} {}", path, size); - fileHandler.truncate(node, size); + if (fi != null) { + fileHandler.ftruncate(size, fi); + } else { + fileHandler.truncate(node, size); + } return 0; } catch (NoSuchFileException e) { LOG.warn("utimens {} failed, file not found.", path); - return -ErrorCodes.ENOENT(); + return -errno.enoent(); } catch (IOException | RuntimeException e) { LOG.error("truncate " + path + " failed.", e); - return -ErrorCodes.EIO(); - } - } - - @Override - public int ftruncate(String path, long size, FuseFileInfo fi) { - try (PathLock pathLock = lockManager.createPathLock(path).forReading(); - DataLock dataLock = pathLock.lockDataForWriting()) { - LOG.trace("ftruncate {} to size: {}", path, size); - fileHandler.ftruncate(size, fi); - return 0; - } catch (ClosedChannelException e) { - LOG.warn("ftruncate {} failed, invalid file handle {}", path, fi.fh.get()); - return -ErrorCodes.EBADF(); - } catch (IOException | RuntimeException e) { - LOG.error("ftruncate " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } @Override - public int fsync(String path, int isdatasync, FuseFileInfo fi) { + public int fsync(String path, int isdatasync, FileInfo fi) { try { boolean metaData = isdatasync == 0; LOG.trace("fsync {}", path); fileHandler.fsync(fi, metaData); return 0; } catch (ClosedChannelException e) { - LOG.warn("fsync {} failed, invalid file handle {}", path, fi.fh.get()); - return -ErrorCodes.EBADF(); + LOG.warn("fsync {} failed, invalid file handle {}", path, fi.getFh()); + return -errno.ebadf(); } catch (IOException | RuntimeException e) { LOG.error("fsync " + path + " failed.", e); - return -ErrorCodes.EIO(); + return -errno.eio(); } } diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteDirectoryHandler.java b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteDirectoryHandler.java index 8be4cc5..a5a690b 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteDirectoryHandler.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteDirectoryHandler.java @@ -1,29 +1,24 @@ package org.cryptomator.frontend.fuse; -import ru.serce.jnrfuse.struct.FileStat; +import org.cryptomator.jfuse.api.Stat; -import javax.inject.Inject; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributes; -@PerAdapter public class ReadWriteDirectoryHandler extends ReadOnlyDirectoryHandler { - @Inject - public ReadWriteDirectoryHandler(FileAttributesUtil attrUtil, FileNameTranscoder fileNameTranscoder) { - super(attrUtil, fileNameTranscoder); + public ReadWriteDirectoryHandler(FileNameTranscoder fileNameTranscoder) { + super(fileNameTranscoder); } @Override - public int getattr(Path node, BasicFileAttributes attrs, FileStat stat) { + public int getattr(Path node, BasicFileAttributes attrs, Stat stat) { int result = super.getattr(node, attrs, stat); - if (attrs instanceof PosixFileAttributes) { - PosixFileAttributes posixAttrs = (PosixFileAttributes) attrs; - long mode = attrUtil.posixPermissionsToOctalMode(posixAttrs.permissions()); - stat.st_mode.set(FileStat.S_IFDIR | mode); + if (attrs instanceof PosixFileAttributes posixAttrs) { + stat.setPermissions(posixAttrs.permissions()); } else { - stat.st_mode.set(FileStat.S_IFDIR | 0755); + stat.setModeBits(0755); } return result; } diff --git a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteFileHandler.java b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteFileHandler.java index 5df263d..e59e30d 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/ReadWriteFileHandler.java +++ b/src/main/java/org/cryptomator/frontend/fuse/ReadWriteFileHandler.java @@ -1,19 +1,16 @@ package org.cryptomator.frontend.fuse; -import jnr.ffi.Pointer; -import ru.serce.jnrfuse.struct.FileStat; -import ru.serce.jnrfuse.struct.FuseFileInfo; -import ru.serce.jnrfuse.struct.Timespec; +import org.cryptomator.jfuse.api.FileInfo; +import org.cryptomator.jfuse.api.Stat; +import org.cryptomator.jfuse.api.TimeSpec; -import javax.inject.Inject; import java.io.Closeable; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; -import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.LinkOption; -import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; @@ -21,44 +18,36 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFileAttributes; -import java.time.Instant; import java.util.EnumSet; import java.util.Set; -@PerAdapter public class ReadWriteFileHandler extends ReadOnlyFileHandler implements Closeable { - private static final long UTIME_NOW = -1l; // https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/bsd/sys/stat.h#L538 - private static final long UTIME_OMIT = -2l; // https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/bsd/sys/stat.h#L539 - - @Inject - public ReadWriteFileHandler(OpenFileFactory openFiles, FileAttributesUtil attrUtil, FileStore fileStore, OpenOptionsUtil openOptionsUtil) { - super(openFiles, attrUtil, openOptionsUtil); + public ReadWriteFileHandler(OpenFileFactory openFiles) { + super(openFiles); } @Override - public int getattr(Path node, BasicFileAttributes attrs, FileStat stat) { + public int getattr(Path node, BasicFileAttributes attrs, Stat stat) { int result = super.getattr(node, attrs, stat); - if (result == 0 && attrs instanceof PosixFileAttributes) { - PosixFileAttributes posixAttrs = (PosixFileAttributes) attrs; - long mode = attrUtil.posixPermissionsToOctalMode(posixAttrs.permissions()); - stat.st_mode.set(FileStat.S_IFREG | mode); + if (result == 0 && attrs instanceof PosixFileAttributes posixAttrs) { + stat.setPermissions(posixAttrs.permissions()); } else if (result == 0) { - stat.st_mode.set(FileStat.S_IFREG | 0777); + stat.setModeBits(0777); } return result; } - public void createAndOpen(Path path, FuseFileInfo fi, FileAttribute... attrs) throws IOException { + public void createAndOpen(Path path, FileInfo fi, FileAttribute... attrs) throws IOException { long fileHandle = openFiles.open(path, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE), attrs); - fi.fh.set(fileHandle); + fi.setFh(fileHandle); } /** * {@inheritDoc} */ @Override - protected long open(Path path, Set openOptions) throws IOException { + protected long open(Path path, Set openOptions) throws IOException { return openFiles.open(path, openOptions); } @@ -73,8 +62,8 @@ protected long open(Path path, Set openOptions) throws IOException { * @throws ClosedChannelException If no open file could be found for the given file handle * @throws IOException If an exception occurs during write. */ - public int write(Pointer buf, long size, long offset, FuseFileInfo fi) throws IOException { - OpenFile file = openFiles.get(fi.fh.get()); + public int write(ByteBuffer buf, long size, long offset, FileInfo fi) throws IOException { + OpenFile file = openFiles.get(fi.getFh()); if (file == null) { throw new ClosedChannelException(); } @@ -89,8 +78,8 @@ public int write(Pointer buf, long size, long offset, FuseFileInfo fi) throws IO * @throws ClosedChannelException If no open file could be found for the given file handle * @throws IOException If an exception occurs during write. */ - public void fsync(FuseFileInfo fi, boolean metaData) throws IOException { - OpenFile file = openFiles.get(fi.fh.get()); + public void fsync(FileInfo fi, boolean metaData) throws IOException { + OpenFile file = openFiles.get(fi.getFh()); if (file == null) { throw new ClosedChannelException(); } @@ -111,30 +100,19 @@ public void truncate(Path path, long size) throws IOException { * @throws ClosedChannelException If no open file could be found for the given file handle * @throws IOException If an exception occurs during write. */ - public void ftruncate(long size, FuseFileInfo fi) throws IOException { - OpenFile file = openFiles.get(fi.fh.get()); + public void ftruncate(long size, FileInfo fi) throws IOException { + OpenFile file = openFiles.get(fi.getFh()); if (file == null) { throw new ClosedChannelException(); } file.truncate(size); } - public void utimens(Path node, Timespec mTimeSpec, Timespec aTimeSpec) throws IOException { - FileTime mTime = toFileTime(mTimeSpec); - FileTime aTime = toFileTime(aTimeSpec); + public void utimens(Path node, TimeSpec mTimeSpec, TimeSpec aTimeSpec) throws IOException { + FileTime mTime = mTimeSpec.getOptional().map(FileTime::from).orElse(null); + FileTime aTime = aTimeSpec.getOptional().map(FileTime::from).orElse(null); BasicFileAttributeView view = Files.getFileAttributeView(node, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); - view.setTimes(mTime, aTime, null); // might fail on JDK < 13, see https://bugs.openjdk.java.net/browse/JDK-8220793 + view.setTimes(mTime, aTime, null); } - private FileTime toFileTime(Timespec timespec) { - long seconds = timespec.tv_sec.longValue(); - long nanoseconds = timespec.tv_nsec.longValue(); - if (nanoseconds == UTIME_NOW) { - return FileTime.from(Instant.now()); - } else if (nanoseconds == UTIME_OMIT) { - return null; - } else { - return FileTime.from(Instant.ofEpochSecond(seconds, nanoseconds)); - } - } } diff --git a/src/main/java/org/cryptomator/frontend/fuse/VersionCompare.java b/src/main/java/org/cryptomator/frontend/fuse/VersionCompare.java deleted file mode 100644 index c52cc8f..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/VersionCompare.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.frontend.fuse; - -/** - * from: https://www.baeldung.com/java-comparing-versions#customSolution - */ -public class VersionCompare { - - public static int compareVersions(String version1, String version2) { - int comparisonResult = 0; - - String[] version1Splits = version1.split("\\."); - String[] version2Splits = version2.split("\\."); - int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); - - for (int i = 0; i < maxLengthOfVersionSplits; i++){ - Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; - Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; - int compare = v1.compareTo(v2); - if (compare != 0) { - comparisonResult = compare; - break; - } - } - return comparisonResult; - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/DataLock.java b/src/main/java/org/cryptomator/frontend/fuse/locks/DataLock.java index 2832658..06991ed 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/DataLock.java +++ b/src/main/java/org/cryptomator/frontend/fuse/locks/DataLock.java @@ -1,7 +1,42 @@ package org.cryptomator.frontend.fuse.locks; -public interface DataLock extends AutoCloseable { +import org.jetbrains.annotations.Unmodifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; + +/** + * A data lock, either for reading (shared) or writing (exclusive). + * + * @param pathComponents The path, split into path components + * @param rwLock The read-write-lock. We need to store a strong reference while in use, because LockManager works with weeak references + * @param lock Either the {@code rwLock}'s read or its write lock + */ +public record DataLock(@Unmodifiable List pathComponents, ReadWriteLock rwLock, Lock lock) implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(DataLock.class); + + static DataLock readLock(List pathComponents, ReadWriteLock rwLock) { + var lock = rwLock.readLock(); + lock.lock(); + LOG.trace("Acquired read data lock for '{}'", pathComponents); + return new DataLock(pathComponents, rwLock, lock); + } + + static DataLock writeLock(List pathComponents, ReadWriteLock rwLock) { + var lock = rwLock.writeLock(); + lock.lock(); + LOG.trace("Acquired write data lock for '{}'", pathComponents); + return new DataLock(pathComponents, rwLock, lock); + } @Override - void close(); + public void close() { + LOG.trace("Released data lock for '{}'", pathComponents); + lock.unlock(); + } + } diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/DataLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/DataLockImpl.java deleted file mode 100644 index 56766dd..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/DataLockImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import java.util.List; -import java.util.concurrent.locks.ReadWriteLock; - -abstract class DataLockImpl implements DataLock { - - protected final List pathComponents; - protected final ReadWriteLock lock; // keep reference to avoid lock being GC'ed out of the LockManager's cache - - protected DataLockImpl(List pathComponents, ReadWriteLock lock) { - this.pathComponents = pathComponents; - this.lock = lock; - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/DataRLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/DataRLockImpl.java deleted file mode 100644 index 296490b..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/DataRLockImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.concurrent.locks.ReadWriteLock; - -class DataRLockImpl extends DataLockImpl { - - private static final Logger LOG = LoggerFactory.getLogger(DataRLockImpl.class); - - private DataRLockImpl(List pathComponents, ReadWriteLock lock) { - super(pathComponents, lock); - } - - static DataRLockImpl create(List pathComponents, ReadWriteLock lock) { - lock.readLock().lock(); - LOG.trace("Acquired read data lock for '{}'", pathComponents); - return new DataRLockImpl(pathComponents, lock); - } - - @Override - public void close() { - LOG.trace("Released read data lock for '{}'", pathComponents); - lock.readLock().unlock(); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/DataWLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/DataWLockImpl.java deleted file mode 100644 index 9832f4c..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/DataWLockImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.concurrent.locks.ReadWriteLock; - -class DataWLockImpl extends DataLockImpl { - - private static final Logger LOG = LoggerFactory.getLogger(DataWLockImpl.class); - - private DataWLockImpl(List pathComponents, ReadWriteLock lock) { - super(pathComponents, lock); - } - - static DataWLockImpl create(List pathComponents, ReadWriteLock lock) { - lock.writeLock().lock(); - LOG.trace("Acquired write data lock for '{}'", pathComponents); - return new DataWLockImpl(pathComponents, lock); - } - - @Override - public void close() { - LOG.trace("Released write data lock for '{}'", pathComponents); - lock.writeLock().unlock(); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/FilePaths.java b/src/main/java/org/cryptomator/frontend/fuse/locks/FilePaths.java index e65d84e..77b5ea0 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/FilePaths.java +++ b/src/main/java/org/cryptomator/frontend/fuse/locks/FilePaths.java @@ -2,10 +2,10 @@ import com.google.common.base.Joiner; import com.google.common.base.Splitter; +import org.jetbrains.annotations.Unmodifiable; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.stream.Stream; class FilePaths { @@ -14,10 +14,9 @@ class FilePaths { private static final Splitter PATH_SPLITTER = Splitter.on(PATH_SEP).omitEmptyStrings(); private static final Joiner PATH_JOINER = Joiner.on(PATH_SEP); + @Unmodifiable public static List toComponents(String pathRelativeToRoot) { - List pathComponents = new ArrayList<>(PATH_SPLITTER.splitToList(pathRelativeToRoot)); - pathComponents.add(0, ROOT); - return Collections.unmodifiableList(pathComponents); + return Stream.concat(Stream.of(ROOT), PATH_SPLITTER.splitToStream(pathRelativeToRoot)).toList(); } public static String toPath(List pathComponents) { diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/LockManager.java b/src/main/java/org/cryptomator/frontend/fuse/locks/LockManager.java index 853960c..3812997 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/LockManager.java +++ b/src/main/java/org/cryptomator/frontend/fuse/locks/LockManager.java @@ -1,19 +1,17 @@ package org.cryptomator.frontend.fuse.locks; -import com.google.common.base.Supplier; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.cache.RemovalNotification; -import org.cryptomator.frontend.fuse.PerAdapter; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.RemovalCause; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; import java.util.List; -import java.util.Optional; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; /** * Provides a path-based locking mechanism as described by @@ -21,25 +19,24 @@ * *

* Usage Example 1: - *

- *     try (PathLock pathLock = lockManager.createPathLock("/foo/bar/baz").forReading(); // path is not manipulated, thus read-locking
- *          DataLock dataLock = pathLock.lockDataForWriting()) { // content is manipulated, thus write-locking
- *          // write to file
+ * {@snippet :
+ *     try (var pathLock = lockManager.lockForReading("/foo/bar/baz"); // path is not manipulated, thus read-locking
+ *          var dataLock = pathLock.lockDataForWriting()) { // content is manipulated, thus write-locking
+ *     		// write to file
  *     }
- * 
+ *} * *

* Usage Example 2: - *

- *     try (PathLock srcPathLock = lockManager.createPathLock("/foo/bar/original").forReading();
- *          DataLock srcDataLock = srcPathLock.lockDataForReading(); // content will only be read, thus read-locking
- *          PathLock dstPathLock = lockManager.createPathLock("/foo/bar/copy").forWriting(); // file will be created, thus write-locking
- *          DataLock dstDataLock = srcPathLock.lockDataForWriting()) {
+ * {@snippet :
+ *     try (var srcPathLock = lockManager.lockForReading("/foo/bar/original");
+ *          var srcDataLock = srcPathLock.lockDataForReading(); // content will only be read, thus read-locking
+ *          var dstPathLock = lockManager.lockForWriting("/foo/bar/copy"); // file will be created, thus write-locking
+ *          var dstDataLock = srcPathLock.lockDataForWriting()) {
  *          // copy from /foo/bar/original to /foo/bar/copy
  *     }
- * 
+ *} */ -@PerAdapter public class LockManager { private static final Logger LOG = LoggerFactory.getLogger(LockManager.class); @@ -47,31 +44,45 @@ public class LockManager { private final LoadingCache, ReadWriteLock> pathLocks; private final LoadingCache, ReadWriteLock> dataLocks; - @Inject public LockManager() { - CacheBuilder cacheBuilder = CacheBuilder.newBuilder().weakValues(); + Caffeine cacheBuilder = Caffeine.newBuilder().weakValues(); if (LOG.isDebugEnabled()) { cacheBuilder.removalListener(this::removedReadWriteLock); } - this.pathLocks = cacheBuilder.build(CacheLoader.from(this::createReadWriteLock)); - this.dataLocks = cacheBuilder.build(CacheLoader.from(this::createReadWriteLock)); + this.pathLocks = cacheBuilder.build(this::createReadWriteLock); + this.dataLocks = cacheBuilder.build(this::createReadWriteLock); } - public PathLockBuilder createPathLock(String path) { - List pathComponents = FilePaths.toComponents(path); - assert !pathComponents.isEmpty(); - return createPathLock(pathComponents); + public PathLock tryLockForWriting(String path) throws AlreadyLockedException { + var pathComponents = FilePaths.toComponents(path); + var lock = lock(pathComponents, PathLock::tryWriteLock); + if (lock != null) { + return lock; + } else { + throw new AlreadyLockedException(); + } + } + + public PathLock lockForReading(String path) { + var pathComponents = FilePaths.toComponents(path); + return lock(pathComponents, PathLock::readLock); + } + + public PathLock lockForWriting(String path) { + var pathComponents = FilePaths.toComponents(path); + return lock(pathComponents, PathLock::writeLock); } - private PathLockBuilder createPathLock(List pathComponents) { + private @Nullable PathLock lock(List pathComponents, PathLock.Factory factory) { if (pathComponents.isEmpty()) { return null; } List parentPathComponents = FilePaths.parentPathComponents(pathComponents); - PathLockBuilder parentLockBuilder = createPathLock(parentPathComponents); - ReadWriteLock lock = pathLocks.getUnchecked(pathComponents); - return new PathLockBuilderImpl(pathComponents, Optional.ofNullable(parentLockBuilder), lock, dataLocks::getUnchecked); + PathLock parentLock = lock(parentPathComponents, PathLock::readLock); + ReadWriteLock rwLock = pathLocks.get(pathComponents); + Supplier dataLockSupplier = () -> dataLocks.get(pathComponents); + return factory.lock(pathComponents, parentLock, rwLock, dataLockSupplier); } private ReadWriteLock createReadWriteLock(List key) { @@ -79,15 +90,15 @@ private ReadWriteLock createReadWriteLock(List key) { return new ReentrantReadWriteLock(); } - private void removedReadWriteLock(RemovalNotification, ReentrantReadWriteLock> notification) { - LOG.trace("Deleting ReadWriteLock for {}", notification.getKey()); + private void removedReadWriteLock(List key, ReentrantReadWriteLock value, RemovalCause removalCause) { + LOG.trace("Deleting ReadWriteLock for {}", key); } /* * Support functions: */ - // visible for testing + @VisibleForTesting boolean isPathLocked(String path) { ReadWriteLock lock = pathLocks.getIfPresent(FilePaths.toComponents(path)); if (lock == null) { diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLock.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathLock.java index c048559..684b00d 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLock.java +++ b/src/main/java/org/cryptomator/frontend/fuse/locks/PathLock.java @@ -1,12 +1,76 @@ package org.cryptomator.frontend.fuse.locks; -public interface PathLock extends AutoCloseable { +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - DataLock lockDataForReading(); +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; - DataLock lockDataForWriting(); +/** + * A path lock, either for reading (shared) or writing (exclusive). + * + * @param pathComponents The path, split into path components + * @param parent The corresponding path lock for the parent path + * @param rwLock The read-write-lock. We need to store a strong reference while in use, because LockManager works with weeak references + * @param lock Either the {@code rwLock}'s read or its write lock + * @param dataLockSupplier A supplier for a separate ReadWriteLock (not {@code rwLock}) to be used during {@link #lockDataForReading()} and {@link #lockDataForWriting()} + */ +public record PathLock(@Unmodifiable List pathComponents, @Nullable PathLock parent, ReadWriteLock rwLock, Lock lock, Supplier dataLockSupplier) implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(PathLock.class); + + @FunctionalInterface + interface Factory { + @Nullable PathLock lock(List pathComponents, @Nullable PathLock parent, ReadWriteLock rwLock, Supplier dataLockSupplier); + } + + static @NotNull PathLock readLock(List pathComponents, @Nullable PathLock parent, ReadWriteLock rwLock, Supplier dataLockSupplier) { + var lock = rwLock.readLock(); + lock.lock(); + LOG.trace("Acquired read path lock for '{}'", pathComponents); + return new PathLock(pathComponents, parent, rwLock, lock, dataLockSupplier); + } + + static @NotNull PathLock writeLock(List pathComponents, @Nullable PathLock parent, ReadWriteLock rwLock, Supplier dataLockSupplier) { + var lock = rwLock.writeLock(); + lock.lock(); + LOG.trace("Acquired write path lock for '{}'", pathComponents); + return new PathLock(pathComponents, parent, rwLock, lock, dataLockSupplier); + } + + static @Nullable PathLock tryWriteLock(List pathComponents, @Nullable PathLock parent, ReadWriteLock rwLock, Supplier dataLockSupplier) { + var lock = rwLock.writeLock(); + if (lock.tryLock()) { + LOG.trace("Acquired write path lock for '{}'", pathComponents); + return new PathLock(pathComponents, parent, rwLock, lock, dataLockSupplier); + } else { + return null; + } + } + + public DataLock lockDataForReading() { + ReadWriteLock dataLock = dataLockSupplier.get(); + return DataLock.readLock(pathComponents, dataLock); + } + + public DataLock lockDataForWriting() { + ReadWriteLock dataLock = dataLockSupplier.get(); + return DataLock.writeLock(pathComponents, dataLock); + } @Override - void close(); + public void close() { + LOG.trace("Released path lock for '{}'", pathComponents); + lock.unlock(); + if (parent != null) { + parent.close(); + } + } } diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilder.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilder.java deleted file mode 100644 index c1a2c64..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -public interface PathLockBuilder { - - PathLock forReading(); - - PathLock tryForReading() throws AlreadyLockedException; - - PathLock forWriting(); - - PathLock tryForWriting() throws AlreadyLockedException; - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilderImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilderImpl.java deleted file mode 100644 index a58f683..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockBuilderImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.function.Function; - -class PathLockBuilderImpl implements PathLockBuilder { - - private final List pathComponents; - private final Optional parent; - private final ReadWriteLock lock; - private final Function, ReadWriteLock> dataLockSupplier; - - PathLockBuilderImpl(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - this.pathComponents = pathComponents; - this.parent = parent; - this.lock = lock; - this.dataLockSupplier = dataLockSupplier; - } - - public PathLock forReading() { - Optional parentLock = parent.map(PathLockBuilder::forReading); - return PathRLockImpl.create(pathComponents, parentLock, lock, dataLockSupplier); - } - - public PathLock tryForReading() throws AlreadyLockedException { - Optional parentLock = parent.map(PathLockBuilder::forReading); - return PathRLockImpl.attempt(pathComponents, parentLock, lock, dataLockSupplier); - } - - public PathLock forWriting() { - Optional parentLock = parent.map(PathLockBuilder::forReading); - return PathWLockImpl.create(pathComponents, parentLock, lock, dataLockSupplier); - } - - public PathLock tryForWriting() throws AlreadyLockedException { - Optional parentLock = parent.map(PathLockBuilder::forReading); - return PathWLockImpl.attempt(pathComponents, parentLock, lock, dataLockSupplier); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockImpl.java deleted file mode 100644 index 34adae8..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathLockImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.function.Function; - -abstract class PathLockImpl implements PathLock { - - protected final List pathComponents; - protected final Optional parent; - protected final ReadWriteLock lock; // keep reference to avoid lock being GC'ed out of the LockManager's cache - private final Function, ReadWriteLock> dataLockSupplier; - - protected PathLockImpl(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - this.pathComponents = pathComponents; - this.parent = parent; - this.lock = lock; - this.dataLockSupplier = dataLockSupplier; - } - - @Override - public void close() { - parent.ifPresent(PathLock::close); - } - - @Override - public DataLock lockDataForReading() { - ReadWriteLock dataLock = dataLockSupplier.apply(pathComponents); - return DataRLockImpl.create(pathComponents, dataLock); - } - - @Override - public DataLock lockDataForWriting() { - ReadWriteLock dataLock = dataLockSupplier.apply(pathComponents); - return DataWLockImpl.create(pathComponents, dataLock); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathRLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathRLockImpl.java deleted file mode 100644 index a987097..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathRLockImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.function.Function; - -class PathRLockImpl extends PathLockImpl { - - private static final Logger LOG = LoggerFactory.getLogger(PathRLockImpl.class); - - private PathRLockImpl(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - super(pathComponents, parent, lock, dataLockSupplier); - } - - public static PathLockImpl create(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - lock.readLock().lock(); - LOG.trace("Acquired read path lock for '{}'", pathComponents); - return new PathRLockImpl(pathComponents, parent, lock, dataLockSupplier); - } - - public static PathLockImpl attempt(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) throws AlreadyLockedException { - if (!lock.readLock().tryLock()) { - throw new AlreadyLockedException(); - } - LOG.trace("Acquired read path lock for '{}'", pathComponents); - return new PathRLockImpl(pathComponents, parent, lock, dataLockSupplier); - } - - @Override - public void close() { - LOG.trace("Released read path lock for '{}'", pathComponents); - lock.readLock().unlock(); - super.close(); - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/locks/PathWLockImpl.java b/src/main/java/org/cryptomator/frontend/fuse/locks/PathWLockImpl.java deleted file mode 100644 index fc4e544..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/locks/PathWLockImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.frontend.fuse.locks; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.function.Function; - -class PathWLockImpl extends PathLockImpl { - - private static final Logger LOG = LoggerFactory.getLogger(PathWLockImpl.class); - - private PathWLockImpl(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - super(pathComponents, parent, lock, dataLockSupplier); - } - - public static PathLockImpl create(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) { - lock.writeLock().lock(); - LOG.trace("Acquired write path lock for '{}'", pathComponents); - return new PathWLockImpl(pathComponents, parent, lock, dataLockSupplier); - } - - public static PathLockImpl attempt(List pathComponents, Optional parent, ReadWriteLock lock, Function, ReadWriteLock> dataLockSupplier) throws AlreadyLockedException { - if (!lock.writeLock().tryLock()) { - throw new AlreadyLockedException(); - } - LOG.trace("Acquired write path lock for '{}'", pathComponents); - return new PathWLockImpl(pathComponents, parent, lock, dataLockSupplier); - } - - @Override - public void close() { - LOG.trace("Released write path lock for '{}'", pathComponents); - lock.writeLock().unlock(); - super.close(); - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMacMountBuilder.java b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMacMountBuilder.java new file mode 100644 index 0000000..05d0a6f --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMacMountBuilder.java @@ -0,0 +1,40 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.cryptomator.integrations.mount.MountBuilder; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +abstract class AbstractMacMountBuilder extends AbstractMountBuilder { + + protected boolean readOnly; + + public AbstractMacMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setReadOnly(boolean mountReadOnly) { + this.readOnly = mountReadOnly; + return this; + } + + /** + * Combines the {@link #setMountFlags(String) mount flags} with any additional option that might have + * been set separately. + * + * @return Mutable set of all currently set mount options + */ + protected Set combinedMountFlags() { + var combined = super.combinedMountFlags(); + if (readOnly) { + combined.add("-r"); + } + if (volumeName != null && !volumeName.isBlank()) { + combined.add("-ovolname=" + volumeName); + } + return combined; + } + +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMount.java b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMount.java index 63bfe90..20b05f9 100644 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMount.java +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMount.java @@ -1,59 +1,41 @@ package org.cryptomator.frontend.fuse.mount; -import com.google.common.base.Preconditions; import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.Mountpoint; +import org.cryptomator.integrations.mount.UnmountFailedException; +import org.cryptomator.jfuse.api.Fuse; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import java.io.IOException; import java.nio.file.Path; +import java.util.concurrent.TimeoutException; abstract class AbstractMount implements Mount { - protected final FuseNioAdapter fuseAdapter; - protected final Path mountPoint; + protected final Fuse fuse; + protected final FuseNioAdapter fuseNioAdapter; + protected final Path mountpoint; - public AbstractMount(FuseNioAdapter fuseAdapter, Path mountPoint) { - Preconditions.checkArgument(fuseAdapter.isMounted()); - this.fuseAdapter = fuseAdapter; - this.mountPoint = mountPoint; + public AbstractMount(Fuse fuse, FuseNioAdapter fuseNioAdapter, Path mountpoint) { + this.fuse = fuse; + this.fuseNioAdapter = fuseNioAdapter; + this.mountpoint = mountpoint; } @Override - public Path getMountPoint() { - Preconditions.checkState(fuseAdapter.isMounted(), "Not currently mounted."); - return mountPoint; + public Mountpoint getMountpoint() { + return Mountpoint.forPath(mountpoint); } @Override - public void reveal(Revealer revealer) throws Exception { - revealer.reveal(mountPoint); - } - - @Override - public void unmount() throws FuseMountException { - if (fuseAdapter.isInUse()) { - throw new FuseMountException("Unmount refused: There are open files or pending operations."); - } - - unmountInternal(); - } - - @Override - public void unmountForced() throws FuseMountException { - unmountForcedInternal(); - } - - protected abstract void unmountInternal() throws FuseMountException; - - protected abstract void unmountForcedInternal() throws FuseMountException; - - @Override - public void close() throws FuseMountException { - if (this.fuseAdapter.isMounted()) { - throw new IllegalStateException("Can not close file system adapter while still mounted."); - } + @MustBeInvokedByOverriders + public void close() throws UnmountFailedException, IOException { try { - this.fuseAdapter.close(); - } catch (Exception e) { - throw new FuseMountException(e); + fuse.close(); + fuseNioAdapter.close(); + } catch (TimeoutException e) { + throw new UnmountFailedException("Fuse loop shutdown timed out.", e); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMountBuilder.java b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMountBuilder.java new file mode 100644 index 0000000..96d6637 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMountBuilder.java @@ -0,0 +1,56 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.cryptomator.integrations.mount.MountBuilder; +import org.jetbrains.annotations.MustBeInvokedByOverriders; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +abstract class AbstractMountBuilder implements MountBuilder { + + protected final Path vfsRoot; + protected Path mountPoint; + protected Set mountFlags; + protected String volumeName; + + public AbstractMountBuilder(Path vfsRoot) { + this.vfsRoot = vfsRoot; + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + this.mountPoint = mountPoint; + return this; + } + + @Override + public MountBuilder setMountFlags(String mountFlagsString) { + // we assume that each flag starts with "-" + var notEmpty = Predicate.not(String::isBlank); + this.mountFlags = Pattern.compile("\\s+-").splitAsStream(" "+mountFlagsString).filter(notEmpty).map("-"::concat).collect(Collectors.toUnmodifiableSet()); + return this; + } + + @Override + public MountBuilder setVolumeName(String volumeName) { + this.volumeName = volumeName; + return this; + } + + /** + * Combines the {@link #setMountFlags(String) mount flags} with any additional option that might have + * been set separately via {@link #setVolumeName(String)}, {@link #setReadOnly(boolean)} and the like. + * + * @return Mutable set of all currently set mount options + */ + @MustBeInvokedByOverriders + protected Set combinedMountFlags() { + var combined = new HashSet<>(mountFlags); + return combined; + } + +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMounter.java b/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMounter.java deleted file mode 100644 index 80a0338..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/AbstractMounter.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.AdapterFactory; -import org.cryptomator.frontend.fuse.FuseNioAdapter; - -import java.nio.file.Path; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -public abstract class AbstractMounter implements Mounter { - - private static final int MOUNT_TIMEOUT_MILLIS = Integer.getInteger("org.cryptomator.frontend.fuse.mountTimeOut",10000); - private static final AtomicInteger MOUNT_COUNTER = new AtomicInteger(0); - - @Override - public synchronized Mount mount(Path directory, EnvironmentVariables envVars, Consumer onFuseExit, boolean debug) throws FuseMountException { - AtomicReference exception = new AtomicReference<>(); - FuseNioAdapter fuseAdapter = AdapterFactory.createReadWriteAdapter(directory, // - AdapterFactory.DEFAULT_MAX_FILENAMELENGTH, // - envVars.getFileNameTranscoder()); - //real mount op - var mountThread = new Thread(() -> { - try { - fuseAdapter.mount(envVars.getMountPoint(), true, debug, envVars.getFuseFlags()); - } catch (Exception e) { - exception.set(e); - } finally { - onFuseExit.accept(exception.get()); - } - }); - mountThread.setName("fuseMount-" + MOUNT_COUNTER.getAndIncrement() + "-main"); - mountThread.setDaemon(true); - mountThread.start(); - - // wait for mounted() is called, unlocking the barrier - try { - fuseAdapter.awaitInitCall(MOUNT_TIMEOUT_MILLIS); - return createMountObject(fuseAdapter, envVars); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FuseMountException("Mounting operation interrupted."); - } catch (TimeoutException e) { - if (exception.get() != null) { - throw new FuseMountException(exception.get()); - } else { - throw new FuseMountException(e); - } - } - } - - @Override - public abstract String[] defaultMountFlags(); - - @Override - public abstract boolean isApplicable(); - - protected abstract Mount createMountObject(FuseNioAdapter fuseNioAdapter, EnvironmentVariables envVars); -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariables.java b/src/main/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariables.java deleted file mode 100644 index ceaf8ac..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariables.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.FileNameTranscoder; - -import java.nio.file.Path; -import java.util.Optional; - -public class EnvironmentVariables { - - private final Path mountPoint; - private final FileNameTranscoder fileNameTranscoder; - private final String[] fuseFlags; - - private EnvironmentVariables(Path mountPoint, String[] fuseFlags, FileNameTranscoder fileNameTranscoder) { - this.mountPoint = mountPoint; - this.fuseFlags = fuseFlags; - this.fileNameTranscoder = fileNameTranscoder; - } - - public static EnvironmentVariablesBuilder create() { - return new EnvironmentVariablesBuilder(); - } - - public Path getMountPoint() { - return mountPoint; - } - - public String[] getFuseFlags() { - return fuseFlags; - } - - public FileNameTranscoder getFileNameTranscoder() { - return fileNameTranscoder; - } - - public static class EnvironmentVariablesBuilder { - - private Path mountPoint = null; - private String[] fuseFlags; - private FileNameTranscoder fileNameTranscoder; - - public EnvironmentVariablesBuilder withMountPoint(Path mountPoint) { - this.mountPoint = mountPoint; - return this; - } - - public EnvironmentVariablesBuilder withFlags(String[] fuseFlags) { - this.fuseFlags = fuseFlags; - return this; - } - - public EnvironmentVariablesBuilder withFileNameTranscoder(FileNameTranscoder fileNameTranscoder) { - this.fileNameTranscoder = fileNameTranscoder; - return this; - } - - public EnvironmentVariables build() { - return new EnvironmentVariables(mountPoint, fuseFlags, fileNameTranscoder); - } - - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountComponent.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountComponent.java deleted file mode 100644 index 214e2aa..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountComponent.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import dagger.Component; - -import java.util.Optional; - -@Component(modules = FuseMountModule.class) -interface FuseMountComponent { - - Optional applicableMounter(); - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountException.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountException.java deleted file mode 100644 index 5bf73c5..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountException.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -public class FuseMountException extends Exception { - - public FuseMountException(String message) { - super(message); - } - - public FuseMountException(Throwable cause) { - super(cause); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountFactory.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountFactory.java deleted file mode 100644 index c5a283b..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -public class FuseMountFactory { - - private static FuseMountComponent COMP = DaggerFuseMountComponent.create(); - - /** - * @return Mounter applicable on the current OS. - * @throws FuseNotSupportedException if the underlying OS does not support FUSE or the specific FUSE driver could not be found - */ - public static Mounter getMounter() throws FuseNotSupportedException { - return COMP.applicableMounter().orElseThrow(FuseNotSupportedException::new); - } - - public static boolean isFuseSupported() { - return COMP.applicableMounter().isPresent(); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountModule.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountModule.java deleted file mode 100644 index cee3765..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseMountModule.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import dagger.Module; -import dagger.Provides; -import dagger.multibindings.IntoSet; - -import java.util.Optional; -import java.util.Set; - -@Module -class FuseMountModule { - - @Provides - @IntoSet - public static Mounter provideLinuxEnvironment() { - return new LinuxMounter(); - } - - @Provides - @IntoSet - public static Mounter provideMacFuseEnvironment() { - return new MacMounter(); - } - - @Provides - @IntoSet - public static Mounter provideWindowsFuseEnvironment() { - return new WindowsMounter(); - } - - @Provides - public static Optional provideEnvironment(Set envs) { - return envs.stream().filter(Mounter::isApplicable).findAny(); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseNotSupportedException.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseNotSupportedException.java deleted file mode 100644 index 13f35ea..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseNotSupportedException.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -public class FuseNotSupportedException extends UnsupportedOperationException { - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/FuseTMountProvider.java b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseTMountProvider.java new file mode 100644 index 0000000..930a53f --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/FuseTMountProvider.java @@ -0,0 +1,125 @@ +package org.cryptomator.frontend.fuse.mount; + +import com.google.common.base.Preconditions; +import org.cryptomator.frontend.fuse.FileNameTranscoder; +import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.frontend.fuse.ReadWriteAdapter; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.MountBuilder; +import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountFailedException; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.jfuse.api.Fuse; +import org.cryptomator.jfuse.api.FuseMountFailedException; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.Normalizer; +import java.util.EnumSet; +import java.util.Set; + +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_FLAGS; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR; +import static org.cryptomator.integrations.mount.MountCapability.READ_ONLY; +import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED; +import static org.cryptomator.integrations.mount.MountCapability.VOLUME_NAME; + +/** + * Mounts a file system on macOS using fuse-t. + * + * @see fuse-t website + */ +@Priority(90) +@OperatingSystem(OperatingSystem.Value.MAC) +public class FuseTMountProvider implements MountService { + + private static final String DYLIB_PATH = "/usr/local/lib/libfuse-t.dylib"; + + @Override + public String displayName() { + return "FUSE-T (Experimental)"; + } + + @Override + public boolean isSupported() { + return Files.exists(Paths.get(DYLIB_PATH)); + } + + @Override + public MountBuilder forFileSystem(Path fileSystemRoot) { + return new FuseTMountBuilder(fileSystemRoot); + } + + @Override + public Set capabilities() { + return EnumSet.of(MOUNT_FLAGS, UNMOUNT_FORCED, READ_ONLY, MOUNT_TO_EXISTING_DIR, VOLUME_NAME); // LOOPBACK_PORT is currently broken + } + + @Override + public int getDefaultLoopbackPort() { + return 2049; + } + + @Override + public String getDefaultMountFlags() { + // see: https://github.com/macos-fuse-t/fuse-t/wiki#supported-mount-options + return "-orwsize=262144"; + } + + private static class FuseTMountBuilder extends AbstractMacMountBuilder { + + private int port; + + public FuseTMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + if (Files.isDirectory(mountPoint)) { // MOUNT_TO_EXISTING_DIR + this.mountPoint = mountPoint; + return this; + } else { + throw new IllegalArgumentException("mount point must be an existing directory"); + } + } + + @Override + public MountBuilder setLoopbackPort(int port) { + this.port = port; + return this; + } + + @Override + protected Set combinedMountFlags() { + Set combined = super.combinedMountFlags(); + // TODO: this is currently broken in fuse-t. we need to stick with the standard port +// if (port != 0) { +// combined.add("-l 0:" + port); +// } + return combined; + } + + @Override + public Mount mount() throws MountFailedException { + Preconditions.checkNotNull(mountPoint); + Preconditions.checkNotNull(mountFlags); + + var builder = Fuse.builder(); + builder.setLibraryPath(DYLIB_PATH); + var filenameTranscoder = FileNameTranscoder.transcoder().withFuseNormalization(Normalizer.Form.NFD); + var fuseAdapter = ReadWriteAdapter.create(builder.errno(), vfsRoot, FuseNioAdapter.DEFAULT_MAX_FILENAMELENGTH, filenameTranscoder); + var fuse = builder.build(fuseAdapter); + try { + fuse.mount("fuse-nio-adapter", mountPoint, combinedMountFlags().toArray(String[]::new)); + return new MacMountedVolume(fuse, fuseAdapter, mountPoint); + } catch (FuseMountFailedException e) { + throw new MountFailedException(e); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxFuseMountProvider.java b/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxFuseMountProvider.java new file mode 100644 index 0000000..8bbb85c --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxFuseMountProvider.java @@ -0,0 +1,160 @@ +package org.cryptomator.frontend.fuse.mount; + +import com.google.common.base.Preconditions; +import org.cryptomator.frontend.fuse.FileNameTranscoder; +import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.frontend.fuse.ReadWriteAdapter; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.MountBuilder; +import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountFailedException; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.integrations.mount.UnmountFailedException; +import org.cryptomator.jfuse.api.Fuse; +import org.cryptomator.jfuse.api.FuseMountFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_FLAGS; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR; + +/** + * Mounts a file system on Linux using libfuse3. + */ +@Priority(100) +@OperatingSystem(OperatingSystem.Value.LINUX) +public class LinuxFuseMountProvider implements MountService { + + private static final Logger LOG = LoggerFactory.getLogger(LinuxFuseMountProvider.class); + private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); + private static final String[] LIB_PATHS = { + "/usr/lib/libfuse3.so", // default + "/lib/x86_64-linux-gnu/libfuse3.so.3", // debian amd64 + "/lib/aarch64-linux-gnu/libfuse3.so.3", // debiant aarch64 + "/usr/lib64/libfuse3.so.3", // fedora + "/app/lib/libfuse3.so" // flatpak + }; + + @Override + public String displayName() { + return "FUSE"; + } + + @Override + public boolean isSupported() { + return Arrays.stream(LIB_PATHS).map(Path::of).anyMatch(Files::exists); + } + + @Override + public Set capabilities() { + return EnumSet.of(MOUNT_FLAGS, MOUNT_TO_EXISTING_DIR); + } + + @Override + public MountBuilder forFileSystem(Path fileSystemRoot) { + return new LinuxFuseMountBuilder(fileSystemRoot); + } + + @Override + public String getDefaultMountFlags() { + // see: https://man7.org/linux/man-pages/man8/mount.fuse3.8.html + try { + return "-oauto_unmount" // + + " -ouid=" + Files.getAttribute(USER_HOME, "unix:uid") // + + " -ogid=" + Files.getAttribute(USER_HOME, "unix:gid") // + + " -oattr_timeout=5"; // + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static class LinuxFuseMountBuilder extends AbstractMountBuilder { + + public LinuxFuseMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + if (Files.isDirectory(mountPoint)) { // MOUNT_TO_EXISTING_DIR + this.mountPoint = mountPoint; + return this; + } else { + throw new IllegalArgumentException("mount point must be an existing directory"); + } + } + + @Override + public Mount mount() throws MountFailedException { + Preconditions.checkNotNull(mountPoint); + Preconditions.checkNotNull(mountFlags); + + var libPath = Arrays.stream(LIB_PATHS).map(Path::of).filter(Files::exists).map(Path::toString).findAny().orElseThrow(); + var builder = Fuse.builder(); + builder.setLibraryPath(libPath); + if (mountFlags.contains("-oallow_other") || mountFlags.contains("-oallow_root")) { + LOG.warn("Mounting with flag -oallow_other or -oallow_root. Ensure that in /etc/fuse.conf option user_allow_other is enabled."); + } + var fuseAdapter = ReadWriteAdapter.create(builder.errno(), vfsRoot, FuseNioAdapter.DEFAULT_MAX_FILENAMELENGTH, FileNameTranscoder.transcoder()); + var fuse = builder.build(fuseAdapter); + try { + fuse.mount("fuse-nio-adapter", mountPoint, mountFlags.toArray(String[]::new)); + return new LinuxFuseMountedVolume(fuse, fuseAdapter, mountPoint); + } catch (FuseMountFailedException e) { + throw new MountFailedException(e); + } + } + + private static class LinuxFuseMountedVolume extends AbstractMount { + private boolean unmounted; + + public LinuxFuseMountedVolume(Fuse fuse, FuseNioAdapter fuseNioAdapter, Path mountpoint) { + super(fuse, fuseNioAdapter, mountpoint); + } + + @Override + public void unmount() throws UnmountFailedException { + ProcessBuilder command = new ProcessBuilder("fusermount3", "-u", "--", mountpoint.getFileName().toString()); + command.directory(mountpoint.getParent().toFile()); + try { + Process p = command.start(); + ProcessHelper.waitForSuccess(p, 10, "`fusermount3 -u`"); + fuse.close(); + unmounted = true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnmountFailedException(e); + } catch (TimeoutException | IOException e) { + throw new UnmountFailedException(e); + } catch (ProcessHelper.CommandFailedException e) { + if (e.stderr.contains(String.format("not mounted", mountpoint)) || e.stderr.contains(String.format("entry for %s not found in", mountpoint))) { + LOG.info("{} already unmounted. Nothing to do.", mountpoint); + } else { + LOG.warn("{} failed with exit code {}:\nSTDOUT: {}\nSTDERR: {}\n", "`fusermount3 -u`", e.exitCode, e.stdout, e.stderr); + throw new UnmountFailedException(e); + } + } + } + + @Override + public void close() throws UnmountFailedException, IOException { + if (!unmounted) { + unmount(); + } + super.close(); + } + } + } +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxMounter.java b/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxMounter.java deleted file mode 100644 index e9569bc..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/LinuxMounter.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.FuseNioAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; - -class LinuxMounter extends AbstractMounter { - - private static final Logger LOG = LoggerFactory.getLogger(LinuxMounter.class); - private static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().contains("linux"); - private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); - - @Override - public String[] defaultMountFlags() { - try { - return new String[]{ - "-ouid=" + Files.getAttribute(USER_HOME, "unix:uid"), - "-ogid=" + Files.getAttribute(USER_HOME, "unix:gid"), - "-oauto_unmount" - }; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public boolean isApplicable() { - return IS_LINUX; - } - - @Override - protected Mount createMountObject(FuseNioAdapter fuseNioAdapter, EnvironmentVariables envVars) { - return new LinuxMount(fuseNioAdapter, envVars); - } - - private static class LinuxMount extends AbstractMount { - - private LinuxMount(FuseNioAdapter fuseAdapter, EnvironmentVariables envVars) { - super(fuseAdapter, envVars.getMountPoint()); - } - - @Override - public void unmountInternal() throws FuseMountException { - if (!fuseAdapter.isMounted()) { - return; - } - ProcessBuilder command = new ProcessBuilder("fusermount", "-u", "--", mountPoint.getFileName().toString()); - command.directory(mountPoint.getParent().toFile()); - Process proc = ProcessUtil.startAndWaitFor(command, 5, TimeUnit.SECONDS); - assertUmountSucceeded(proc); - fuseAdapter.setUnmounted(); - } - - @Override - public void unmountForcedInternal() throws FuseMountException { - if (!fuseAdapter.isMounted()) { - return; - } - ProcessBuilder command = new ProcessBuilder("fusermount", "-u", "-z", "--", mountPoint.getFileName().toString()); - command.directory(mountPoint.getParent().toFile()); - Process proc = ProcessUtil.startAndWaitFor(command, 5, TimeUnit.SECONDS); - assertUmountSucceeded(proc); - fuseAdapter.setUnmounted(); - } - - private void assertUmountSucceeded(Process proc) throws FuseMountException { - if (proc.exitValue() == 0) { - return; - } - try { - String stderr = ProcessUtil.toString(proc.getErrorStream(), StandardCharsets.US_ASCII).toLowerCase(); - if (stderr.contains("not mounted") || stderr.contains("no such file or directory")) { - LOG.info("Already unmounted"); - return; - } else { - throw new FuseMountException("Unmount failed. STDERR: " + stderr); - } - } catch (IOException e) { - throw new FuseMountException(e); - } - } - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/MacFuseMountProvider.java b/src/main/java/org/cryptomator/frontend/fuse/mount/MacFuseMountProvider.java new file mode 100644 index 0000000..52feba8 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/MacFuseMountProvider.java @@ -0,0 +1,129 @@ +package org.cryptomator.frontend.fuse.mount; + +import com.google.common.base.Preconditions; +import org.cryptomator.frontend.fuse.FileNameTranscoder; +import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.frontend.fuse.ReadWriteAdapter; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.MountBuilder; +import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountFailedException; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.jfuse.api.Fuse; +import org.cryptomator.jfuse.api.FuseMountFailedException; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.Normalizer; +import java.util.EnumSet; +import java.util.Set; + +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_FLAGS; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH; +import static org.cryptomator.integrations.mount.MountCapability.READ_ONLY; +import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED; +import static org.cryptomator.integrations.mount.MountCapability.VOLUME_ID; +import static org.cryptomator.integrations.mount.MountCapability.VOLUME_NAME; + +/** + * Mounts a file system on macOS using macFUSE. + * + * @see macFUSE website + */ +@Priority(100) +@OperatingSystem(OperatingSystem.Value.MAC) +public class MacFuseMountProvider implements MountService { + + private static final String DYLIB_PATH = "/usr/local/lib/libosxfuse.2.dylib"; + private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); + + @Override + public String displayName() { + return "macFUSE"; + } + + @Override + public boolean isSupported() { + return Files.exists(Paths.get(DYLIB_PATH)); + } + + @Override + public MountBuilder forFileSystem(Path fileSystemRoot) { + return new MacFuseMountBuilder(fileSystemRoot); + } + + @Override + public Set capabilities() { + return EnumSet.of(MOUNT_FLAGS, UNMOUNT_FORCED, READ_ONLY, MOUNT_TO_EXISTING_DIR, MOUNT_TO_SYSTEM_CHOSEN_PATH, VOLUME_ID, VOLUME_NAME); + } + + @Override + public String getDefaultMountFlags() { + // see: https://github.com/osxfuse/osxfuse/wiki/Mount-options + try { + return " -ouid=" + Files.getAttribute(USER_HOME, "unix:uid") // + + " -ogid=" + Files.getAttribute(USER_HOME, "unix:gid") // + + " -oatomic_o_trunc" // + + " -oauto_xattr" // + + " -oauto_cache" // + + " -onoappledouble" // vastly impacts performance for some reason... + + " -odefault_permissions"; // let the kernel assume permissions based on file attributes etc + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static class MacFuseMountBuilder extends AbstractMacMountBuilder { + + private String volumeId; + + public MacFuseMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + if (mountPoint.startsWith("/Volumes/") && Files.notExists(mountPoint) // MOUNT_TO_SYSTEM_CHOSEN_PATH + || Files.isDirectory(mountPoint)) { // MOUNT_TO_EXISTING_DIR + this.mountPoint = mountPoint; + } else { + throw new IllegalArgumentException("mount point must be an existing directory"); + } + return this; + } + + @Override + public MountBuilder setVolumeId(String volumeId) { + this.volumeId = volumeId; + return this; + } + + @Override + public Mount mount() throws MountFailedException { + Preconditions.checkNotNull(mountFlags); + if (mountPoint == null) { + Preconditions.checkNotNull(volumeId); + mountPoint = Path.of("/Volumes/", volumeId); + } + + var builder = Fuse.builder(); + builder.setLibraryPath(DYLIB_PATH); + var filenameTranscoder = FileNameTranscoder.transcoder().withFuseNormalization(Normalizer.Form.NFD); + var fuseAdapter = ReadWriteAdapter.create(builder.errno(), vfsRoot, FuseNioAdapter.DEFAULT_MAX_FILENAMELENGTH, filenameTranscoder); + var fuse = builder.build(fuseAdapter); + try { + fuse.mount("fuse-nio-adapter", mountPoint, combinedMountFlags().toArray(String[]::new)); + return new MacMountedVolume(fuse, fuseAdapter, mountPoint); + } catch (FuseMountFailedException e) { + throw new MountFailedException(e); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/MacMountedVolume.java b/src/main/java/org/cryptomator/frontend/fuse/mount/MacMountedVolume.java new file mode 100644 index 0000000..7c57de6 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/MacMountedVolume.java @@ -0,0 +1,65 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.integrations.mount.UnmountFailedException; +import org.cryptomator.jfuse.api.Fuse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.TimeoutException; + +class MacMountedVolume extends AbstractMount { + + private static final Logger LOG = LoggerFactory.getLogger(MacMountedVolume.class); + + private boolean unmounted; + + public MacMountedVolume(Fuse fuse, FuseNioAdapter adapter, Path mountPoint) { + super(fuse, adapter, mountPoint); + } + + @Override + public void unmount() throws UnmountFailedException { + ProcessBuilder command = new ProcessBuilder("umount", "--", mountpoint.getFileName().toString()); + command.directory(mountpoint.getParent().toFile()); + unmount(command, "`umount`"); + } + + @Override + public void unmountForced() throws UnmountFailedException { + ProcessBuilder command = new ProcessBuilder("umount", "-f", "--", mountpoint.getFileName().toString()); + command.directory(mountpoint.getParent().toFile()); + unmount(command, "`umount -f`"); + } + + private void unmount(ProcessBuilder command, String cmdDescription) throws UnmountFailedException { + try { + Process p = command.start(); + ProcessHelper.waitForSuccess(p, 10, cmdDescription); + fuse.close(); + unmounted = true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnmountFailedException(e); + } catch (TimeoutException | IOException e) { + throw new UnmountFailedException(e); + } catch (ProcessHelper.CommandFailedException e) { + if (e.stderr.contains("not currently mounted")) { + LOG.info("{} already unmounted. Nothing to do.", mountpoint); + } else { + LOG.warn("{} failed with exit code {}:\nSTDOUT: {}\nSTDERR: {}\n", cmdDescription, e.exitCode, e.stdout, e.stderr); + throw new UnmountFailedException(e); + } + } + } + + @Override + public void close() throws UnmountFailedException, IOException { + if (!unmounted) { + unmountForced(); + } + super.close(); + } +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/MacMounter.java b/src/main/java/org/cryptomator/frontend/fuse/mount/MacMounter.java deleted file mode 100644 index c49c271..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/MacMounter.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.FileNameTranscoder; -import org.cryptomator.frontend.fuse.FuseNioAdapter; -import org.cryptomator.frontend.fuse.VersionCompare; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathException; -import javax.xml.xpath.XPathFactory; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.text.Normalizer; -import java.util.concurrent.TimeUnit; - -class MacMounter extends AbstractMounter { - - private static final Logger LOG = LoggerFactory.getLogger(MacMounter.class); - private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac"); - private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); - private static final String MACFUSE_MINIMUM_SUPPORTED_VERSION = "4.0.4"; - private static final String MACFUSE_VERSIONFILE_LOCATION = "/Library/Filesystems/macfuse.fs/Contents/version.plist"; - private static final String MACFUSE_VERSIONFILE_XPATH = "/plist/dict/key[.='CFBundleShortVersionString']/following-sibling::string[1]"; - private static final String PLIST_DTD_URL = "http://www.apple.com/DTDs/PropertyList-1.0.dtd"; - - @Override - public String[] defaultMountFlags() { - // see: https://github.com/osxfuse/osxfuse/wiki/Mount-options - try { - return new String[]{ - "-ouid=" + Files.getAttribute(USER_HOME, "unix:uid"), - "-ogid=" + Files.getAttribute(USER_HOME, "unix:gid"), - "-oatomic_o_trunc", - "-oauto_xattr", - "-oauto_cache", - "-onoappledouble", // vastly impacts performance for some reason... - "-odefault_permissions" // let the kernel assume permissions based on file attributes etc - }; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public FileNameTranscoder defaultFileNameTranscoder() { - return FileNameTranscoder.transcoder().withFuseNormalization(Normalizer.Form.NFD); - } - - @Override - public boolean isApplicable() { - return IS_MAC && Files.exists(Paths.get("/usr/local/lib/libosxfuse.2.dylib")) && installedVersionSupported(); - } - - @Override - protected Mount createMountObject(FuseNioAdapter fuseNioAdapter, EnvironmentVariables envVars) { - return new MacMount(fuseNioAdapter, envVars); - } - - public boolean installedVersionSupported() { - String installedVersion = getInstalledVersion(MACFUSE_VERSIONFILE_LOCATION, MACFUSE_VERSIONFILE_XPATH); - if (installedVersion == null) { - return false; - } else { - return VersionCompare.compareVersions(installedVersion, MACFUSE_MINIMUM_SUPPORTED_VERSION) >= 0; - } - } - - private String getInstalledVersion(String plistFileLocation, String versionXPath) { - Path plistFile = Paths.get(plistFileLocation); - DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); - XPath xPath = XPathFactory.newInstance().newXPath(); - try (InputStream in = Files.newInputStream(plistFile, StandardOpenOption.READ)) { - DocumentBuilder docBuilder = domFactory.newDocumentBuilder(); - docBuilder.setEntityResolver(this::resolveEntity); - Document doc = docBuilder.parse(in); - NodeList nodeList = (NodeList) xPath.compile(versionXPath).evaluate(doc, XPathConstants.NODESET); - Node node = nodeList.item(0); - if (node == null) { - LOG.error("Did not find {} in document {}.", versionXPath, plistFileLocation); - return null; // not found - } else { - return node.getTextContent(); - } - } catch (ParserConfigurationException | SAXException | XPathException e) { - LOG.error("Could not parse " + plistFileLocation + " to detect version of macFUSE.", e); - return null; - } catch (IOException e) { - LOG.error("Could not read " + plistFileLocation + " to detect version of macFUSE.", e); - return null; - } - } - - private InputSource resolveEntity(String publicId, String systemId) { - if (PLIST_DTD_URL.equals(systemId)) { - // load DTD from local resource. fixes https://github.com/cryptomator/fuse-nio-adapter/issues/40 - return new InputSource(getClass().getResourceAsStream("/PropertyList-1.0.dtd")); - } else { - return null; - } - } - - private static class MacMount extends AbstractMount { - - private MacMount(FuseNioAdapter fuseAdapter, EnvironmentVariables envVars) { - super(fuseAdapter, envVars.getMountPoint()); - } - - @Override - public void unmountInternal() throws FuseMountException { - if (!fuseAdapter.isMounted()) { - return; - } - ProcessBuilder command = new ProcessBuilder("umount", "--", mountPoint.getFileName().toString()); - command.directory(mountPoint.getParent().toFile()); - Process proc = ProcessUtil.startAndWaitFor(command, 5, TimeUnit.SECONDS); - assertUmountSucceeded(proc); - fuseAdapter.setUnmounted(); - } - - @Override - public void unmountForcedInternal() throws FuseMountException { - if (!fuseAdapter.isMounted()) { - return; - } - ProcessBuilder command = new ProcessBuilder("umount", "-f", "--", mountPoint.getFileName().toString()); - command.directory(mountPoint.getParent().toFile()); - Process proc = ProcessUtil.startAndWaitFor(command, 5, TimeUnit.SECONDS); - assertUmountSucceeded(proc); - fuseAdapter.setUnmounted(); - } - - private void assertUmountSucceeded(Process proc) throws FuseMountException { - if (proc.exitValue() == 0) { - return; - } - try { - String stderr = ProcessUtil.toString(proc.getErrorStream(), StandardCharsets.US_ASCII); - if (stderr.contains("not currently mounted")) { - LOG.info("Already unmounted"); - return; - } else { - throw new FuseMountException("Unmount failed. STDERR: " + stderr); - } - } catch (IOException e) { - throw new FuseMountException(e); - } - } - - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/Mount.java b/src/main/java/org/cryptomator/frontend/fuse/mount/Mount.java deleted file mode 100644 index 12306c9..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/Mount.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import java.nio.file.Path; - -public interface Mount extends AutoCloseable { - - /** - * Attempts to reveal the mounted FUSE volume. This method may choose to ignore the given revealer. If the revealer is invoked, it must reveal the given path. - * - * @param revealer Object containing necessary commands to show the Mount content to the user. - */ - void reveal(Revealer revealer) throws Exception; - - /** - * Returns this Mount's mount point. - * - * @return The mount point - * @throws IllegalStateException If not currently mounted - */ - Path getMountPoint(); - - /** - * Gracefully attempts to unmount the FUSE volume. - * - * @throws FuseMountException - */ - void unmount() throws FuseMountException; - - /** - * Forcefully unmounts the FUSE volume and releases corresponding resources. - * - * @throws FuseMountException - */ - void unmountForced() throws FuseMountException; - - /** - * Releases associated resources - * - * @throws FuseMountException If closing failed - * @throws IllegalStateException If still mounted - */ - @Override - void close() throws FuseMountException, IllegalStateException; -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/Mounter.java b/src/main/java/org/cryptomator/frontend/fuse/mount/Mounter.java deleted file mode 100644 index e078a1b..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/Mounter.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.FileNameTranscoder; - -import java.nio.file.Path; -import java.util.function.Consumer; - -public interface Mounter { - - default Mount mount(Path directory, EnvironmentVariables envVars) throws FuseMountException { - return mount(directory, envVars, ignored -> {}); - } - - default Mount mount(Path directory, EnvironmentVariables envVars, Consumer onFuseExit) throws FuseMountException { - return mount(directory, envVars, onFuseExit, false); - } - - Mount mount(Path directory, EnvironmentVariables envVars, Consumer onFuseExit, boolean debug) throws FuseMountException; - - String[] defaultMountFlags(); - - boolean isApplicable(); - - default FileNameTranscoder defaultFileNameTranscoder() { - return FileNameTranscoder.transcoder(); - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessHelper.java b/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessHelper.java new file mode 100644 index 0000000..4bc4b10 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessHelper.java @@ -0,0 +1,52 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.jetbrains.annotations.Blocking; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +class ProcessHelper { + + private ProcessHelper() { + } + + /** + * Waits {@code timeoutSeconds} seconds for {@code process} to finish with exit code {@code 0}. + * + * @param process The process to wait for + * @param timeoutSeconds How long to wait (in seconds) + * @param cmdDescription A short description of the process used to generate log and exception messages + * @throws TimeoutException Thrown when the process doesn't finish in time + * @throws InterruptedException Thrown when the thread is interrupted while waiting for the process to finish + * @throws CommandFailedException Thrown when the process exit code is non-zero + */ + @Blocking + static void waitForSuccess(Process process, int timeoutSeconds, String cmdDescription) throws TimeoutException, InterruptedException, CommandFailedException { + boolean exited = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); + if (!exited) { + throw new TimeoutException(cmdDescription + " timed out after " + timeoutSeconds + "s"); + } + if (process.exitValue() != 0) { + @SuppressWarnings("resource") var stdout = process.inputReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n")); + @SuppressWarnings("resource") var stderr = process.errorReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n")); + throw new CommandFailedException(cmdDescription, process.exitValue(), stdout, stderr); + } + } + + static class CommandFailedException extends Exception { + + int exitCode; + String stdout; + String stderr; + + private CommandFailedException(String cmdDescription, int exitCode, String stdout, String stderr) { + super(cmdDescription + " returned with non-zero exit code " + exitCode); + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + + } +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessUtil.java b/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessUtil.java deleted file mode 100644 index 5a25c5b..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/ProcessUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import com.google.common.io.CharStreams; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; - -class ProcessUtil { - - /** - * Fails with a CommandFailedException, if the process did not finish with the expected exit code. - * - * @param proc A finished process - * @param expectedExitValue Exit code returned by the process - * @throws FuseMountException Thrown in case of unexpected exit values - */ - public static void assertExitValue(Process proc, int expectedExitValue) throws FuseMountException { - int actualExitValue = proc.exitValue(); - if (actualExitValue != expectedExitValue) { - try { - String error = toString(proc.getErrorStream(), StandardCharsets.UTF_8); - throw new FuseMountException("Command failed with exit code " + actualExitValue + ". Expected " + expectedExitValue + ". Stderr: " + error); - } catch (IOException e) { - throw new FuseMountException("Command failed with exit code " + actualExitValue + ". Expected " + expectedExitValue + "."); - } - } - } - - /** - * Starts a new process and invokes {@link #waitFor(Process, long, TimeUnit)}. - * - * @param processBuilder The process builder used to start the new process - * @param timeout Maximum time to wait - * @param unit Time unit of timeout - * @return The finished process. - * @throws FuseMountException If an I/O error occurs when starting the process. - * @throws CommandTimeoutException Thrown in case of a timeout - */ - public static Process startAndWaitFor(ProcessBuilder processBuilder, long timeout, TimeUnit unit) throws FuseMountException, CommandTimeoutException { - try { - Process proc = processBuilder.start(); - waitFor(proc, timeout, unit); - return proc; - } catch (IOException e) { - throw new FuseMountException(e); - } - } - - /** - * Waits for the process to terminate or throws an exception if it fails to do so within the given timeout. - * - * @param proc A started process - * @param timeout Maximum time to wait - * @param unit Time unit of timeout - * @throws CommandTimeoutException Thrown in case of a timeout - */ - public static void waitFor(Process proc, long timeout, TimeUnit unit) throws CommandTimeoutException { - try { - boolean finishedInTime = proc.waitFor(timeout, unit); - if (!finishedInTime) { - proc.destroyForcibly(); - throw new CommandTimeoutException(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - public static String toString(InputStream in, Charset charset) throws IOException { - return CharStreams.toString(new InputStreamReader(in, charset)); - } - - public static class CommandTimeoutException extends FuseMountException { - - public CommandTimeoutException() { - super("Command timed out."); - } - - } - -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/Revealer.java b/src/main/java/org/cryptomator/frontend/fuse/mount/Revealer.java deleted file mode 100644 index 11e58e4..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/Revealer.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import java.nio.file.Path; - -@FunctionalInterface -public interface Revealer { - - void reveal(Path path) throws Exception; -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspMountProvider.java b/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspMountProvider.java new file mode 100644 index 0000000..10bdf53 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspMountProvider.java @@ -0,0 +1,158 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.cryptomator.frontend.fuse.FileNameTranscoder; +import org.cryptomator.frontend.fuse.FuseNioAdapter; +import org.cryptomator.frontend.fuse.ReadWriteAdapter; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.MountBuilder; +import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountFailedException; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.integrations.mount.UnmountFailedException; +import org.cryptomator.jfuse.api.Fuse; +import org.cryptomator.jfuse.api.FuseMountFailedException; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +import static org.cryptomator.integrations.mount.MountCapability.FILE_SYSTEM_NAME; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_FLAGS; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT; +import static org.cryptomator.integrations.mount.MountCapability.READ_ONLY; +import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED; +import static org.cryptomator.integrations.mount.MountCapability.VOLUME_NAME; + +@Priority(90) +@OperatingSystem(OperatingSystem.Value.WINDOWS) +public class WinFspMountProvider implements MountService { + + private static final String OS_ARCH = System.getProperty("os.arch").toLowerCase(); + + @Override + public String displayName() { + return "WinFsp (Local Drive)"; + } + + @Override + public boolean isSupported() { + return WinfspUtil.isWinFspInstalled(); + } + + @Override + public Set capabilities() { + return EnumSet.of(MOUNT_FLAGS, MOUNT_AS_DRIVE_LETTER, MOUNT_WITHIN_EXISTING_PARENT, UNMOUNT_FORCED, READ_ONLY, VOLUME_NAME, FILE_SYSTEM_NAME); + } + + @Override + public String getDefaultMountFlags() { + // see: https://github.com/winfsp/winfsp/blob/84b3f98d383b265ebdb33891fc911eaafb878497/src/dll/fuse/fuse.c#L628 + return "-ouid=-1 -ogid=-1"; + } + + @Override + public MountBuilder forFileSystem(Path vfsRoot) { + return new WinFspMountBuilder(vfsRoot); + } + + protected static class WinFspMountBuilder extends AbstractMountBuilder { + + private static String DEFAULT_FS_NAME = "FUSE-NIO-FS"; + String fsName = DEFAULT_FS_NAME; + boolean isReadOnly = false; + + WinFspMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + if (mountPoint.getRoot().equals(mountPoint) // MOUNT_AS_DRIVE_LETTER + || Files.isDirectory(mountPoint.getParent()) && Files.notExists(mountPoint)) { // MOUNT_WITHIN_EXISTING_PARENT + this.mountPoint = mountPoint; + return this; + } else { + throw new IllegalArgumentException("mount point must either be a drive letter or a non-existing node within an existing parent"); + } + } + + @Override + public MountBuilder setFileSystemName(String fsName) { + this.fsName = fsName; + return this; + } + + @Override + public MountBuilder setReadOnly(boolean mountReadOnly) { + isReadOnly = mountReadOnly; + return this; + } + + /** + * Combines the {@link #setMountFlags(String) mount flags} with any additional option that might have + * been set separately. + * + * @return Mutable set of all currently set mount options + */ + protected Set combinedMountFlags() { + var combined = super.combinedMountFlags(); + if (isReadOnly) { + combined.removeIf(flag -> flag.startsWith("-oumask=")); + combined.add("-oumask=0333"); + } + combined.removeIf(flag -> flag.startsWith("-oExactFileSystemName=")); + combined.add("-oExactFileSystemName=" + fsName); + if(volumeName != null && !volumeName.isBlank()) { + combined.removeIf(flag -> flag.startsWith("-ovolname=")); + combined.add("-ovolname=" + volumeName); + } + return combined; + } + + @Override + public Mount mount() throws MountFailedException { + var builder = Fuse.builder(); + var libPath = WinfspUtil.getWinFspInstallDir() + "bin\\" + (OS_ARCH.contains("aarch64") ? "winfsp-a64.dll" : "winfsp-x64.dll"); + builder.setLibraryPath(libPath); + var fuseAdapter = ReadWriteAdapter.create(builder.errno(), vfsRoot, FuseNioAdapter.DEFAULT_MAX_FILENAMELENGTH, FileNameTranscoder.transcoder()); + try { + var fuse = builder.build(fuseAdapter); + fuse.mount("fuse-nio-adapter", mountPoint, combinedMountFlags().toArray(String[]::new)); + return new WinfspMount(fuse, fuseAdapter, mountPoint); + } catch (FuseMountFailedException e) { + throw new MountFailedException(e); + } + } + + } + + private static class WinfspMount extends AbstractMount { + + public WinfspMount(Fuse fuseBinding, FuseNioAdapter fuseNioAdapter, Path mountpoint) { + super(fuseBinding, fuseNioAdapter, mountpoint); + } + + @Override + public void unmount() throws UnmountFailedException { + if (fuseNioAdapter.isInUse()) { + throw new UnmountFailedException("Filesystem in use"); + } + unmountForced(); + } + + @Override + public void unmountForced() throws UnmountFailedException { + try { + fuse.close(); + } catch (TimeoutException e) { + throw new UnmountFailedException(e); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspNetworkMountProvider.java b/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspNetworkMountProvider.java new file mode 100644 index 0000000..8502345 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/WinFspNetworkMountProvider.java @@ -0,0 +1,83 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.mount.MountBuilder; +import org.cryptomator.integrations.mount.MountCapability; + +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +import static org.cryptomator.integrations.mount.MountCapability.LOOPBACK_HOST_NAME; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER; +import static org.cryptomator.integrations.mount.MountCapability.MOUNT_FLAGS; +import static org.cryptomator.integrations.mount.MountCapability.READ_ONLY; +import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED; +import static org.cryptomator.integrations.mount.MountCapability.VOLUME_NAME; + +@Priority(100) +@OperatingSystem(OperatingSystem.Value.WINDOWS) +public class WinFspNetworkMountProvider extends WinFspMountProvider { + + private static final Pattern HOST_NAME_PATTERN_NEGATED = Pattern.compile("[a-zA-Z0-9-._~]+"); // all but unreserved chars according to https://www.rfc-editor.org/rfc/rfc3986#section-2.3 + + @Override + public String displayName() { + return "WinFsp"; + } + + @Override + public Set capabilities() { + // no MOUNT_WITHIN_EXISTING_PARENT support here + return EnumSet.of(MOUNT_FLAGS, MOUNT_AS_DRIVE_LETTER, UNMOUNT_FORCED, READ_ONLY, VOLUME_NAME, LOOPBACK_HOST_NAME); + } + + @Override + public MountBuilder forFileSystem(Path vfsRoot) { + return new WinFspNetworkMountBuilder(vfsRoot); + } + + + private static class WinFspNetworkMountBuilder extends WinFspMountBuilder { + + private String volumeName; + private String loopbackHostName = "localhost"; + + public WinFspNetworkMountBuilder(Path vfsRoot) { + super(vfsRoot); + } + + @Override + public MountBuilder setMountpoint(Path mountPoint) { + if (mountPoint.getRoot().equals(mountPoint)) { // MOUNT_AS_DRIVE_LETTER + this.mountPoint = mountPoint; + return this; + } else { + throw new IllegalArgumentException("mount point must be a drive letter"); + } + } + + @Override + public MountBuilder setLoopbackHostName(String hostName) { + if (HOST_NAME_PATTERN_NEGATED.matcher(hostName).find()) { + throw new IllegalArgumentException("Loopback host may only contain the characters a-z, A-Z, 0-9 and -._~"); + } + this.loopbackHostName = hostName; + return this; + } + + @Override + protected Set combinedMountFlags() { + var combined = super.combinedMountFlags(); + if (volumeName != null && !volumeName.isBlank()) { + combined.add("-oVolumePrefix=/" + loopbackHostName + "/" + volumeName); + } else { + combined.add("-oVolumePrefix=/" + loopbackHostName + "/" + UUID.randomUUID()); + } + return combined; + } + } +} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/WindowsMounter.java b/src/main/java/org/cryptomator/frontend/fuse/mount/WindowsMounter.java deleted file mode 100644 index 61669f5..0000000 --- a/src/main/java/org/cryptomator/frontend/fuse/mount/WindowsMounter.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import jnr.ffi.Platform; -import org.cryptomator.frontend.fuse.FuseNioAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import ru.serce.jnrfuse.FuseException; -import ru.serce.jnrfuse.utils.WinPathUtils; - -class WindowsMounter extends AbstractMounter { - - private static final Logger LOG = LoggerFactory.getLogger(WindowsMounter.class); - private static final boolean IS_APPLICABLE = Platform.getNativePlatform().getOS() == Platform.OS.WINDOWS && isWinFspInstalled(); - - @Override - public String[] defaultMountFlags() { - return new String[]{"-ouid=-1", "-ogid=-1"}; - } - - @Override - public boolean isApplicable() { - return IS_APPLICABLE; - } - - @Override - protected Mount createMountObject(FuseNioAdapter fuseNioAdapter, EnvironmentVariables envVars) { - return new WindowsMount(fuseNioAdapter, envVars); - } - - private static boolean isWinFspInstalled() { - try { - String path = WinPathUtils.getWinFspPath(); //Result only matters for debug-message; null-check is included in lib - LOG.trace("Found WinFsp installation at {}", path); - return true; - } catch (FuseException exc) { - LOG.debug("Failed to find a WinFsp installation; that's only a problem if you want to use FUSE on Windows. Exception text: \"{}\"", exc.getMessage()); - return false; - } - } - - private static class WindowsMount extends AbstractMount { - - private WindowsMount(FuseNioAdapter fuseAdapter, EnvironmentVariables envVars) { - super(fuseAdapter, envVars.getMountPoint()); - } - - @Override - protected void unmountInternal() { - if (!fuseAdapter.isMounted()) { - return; - } - fuseAdapter.umount(); - } - - @Override - protected void unmountForcedInternal() { - unmountInternal(); - } - - } -} diff --git a/src/main/java/org/cryptomator/frontend/fuse/mount/WinfspUtil.java b/src/main/java/org/cryptomator/frontend/fuse/mount/WinfspUtil.java new file mode 100644 index 0000000..76d1a56 --- /dev/null +++ b/src/main/java/org/cryptomator/frontend/fuse/mount/WinfspUtil.java @@ -0,0 +1,67 @@ +package org.cryptomator.frontend.fuse.mount; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility class to determine location of the Winfsp binary. + * It reads WinFsp registry keys and caches the result. + */ +public class WinfspUtil { + + private static final Logger LOG = LoggerFactory.getLogger(WinfspUtil.class); + + private WinfspUtil() { + } + + private static final String REGSTR_TOKEN = "REG_SZ"; + private static final String REG_WINFSP_KEY = "HKLM\\SOFTWARE\\WOW6432Node\\WinFsp"; + private static final String REG_WINFSP_VALUE = "InstallDir"; + + private static final AtomicReference cache = new AtomicReference<>(null); + + static String getWinFspInstallDir() throws WinFspNotFoundException { + if (cache.get() == null) { + cache.set(readWinFspInstallDirFromRegistry()); + } + return cache.get(); + } + + static String readWinFspInstallDirFromRegistry() { + try { + ProcessBuilder command = new ProcessBuilder("reg", "query", REG_WINFSP_KEY, "/v", REG_WINFSP_VALUE); + Process p = command.start(); + ProcessHelper.waitForSuccess(p, 3, "`reg query`"); + String result = p.inputReader(StandardCharsets.UTF_8).lines().filter(l -> l.contains(REG_WINFSP_VALUE)).findFirst().orElseThrow(); + return result.substring(result.indexOf(REGSTR_TOKEN) + REGSTR_TOKEN.length()).trim(); + } catch (TimeoutException | IOException | ProcessHelper.CommandFailedException e) { + throw new WinFspNotFoundException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new WinFspNotFoundException(e); + } + } + + static boolean isWinFspInstalled() { + try { + return Files.exists(Path.of(getWinFspInstallDir())); + } catch (WinFspNotFoundException e) { + return false; + } + } + + static class WinFspNotFoundException extends RuntimeException { + + public WinFspNotFoundException(Exception e) { + super(e); + } + } + +} diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.mount.MountService b/src/main/resources/META-INF/services/org.cryptomator.integrations.mount.MountService new file mode 100644 index 0000000..186d300 --- /dev/null +++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.mount.MountService @@ -0,0 +1,5 @@ +org.cryptomator.frontend.fuse.mount.FuseTMountProvider +org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider +org.cryptomator.frontend.fuse.mount.MacFuseMountProvider +org.cryptomator.frontend.fuse.mount.WinFspMountProvider +org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider \ No newline at end of file diff --git a/src/main/resources/PropertyList-1.0.dtd b/src/main/resources/PropertyList-1.0.dtd deleted file mode 100644 index d6feecd..0000000 --- a/src/main/resources/PropertyList-1.0.dtd +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/java/org/cryptomator/frontend/fuse/AccessPatternIntegrationTest.java b/src/test/java/org/cryptomator/frontend/fuse/AccessPatternIntegrationTest.java index e6efd3b..3983878 100644 --- a/src/test/java/org/cryptomator/frontend/fuse/AccessPatternIntegrationTest.java +++ b/src/test/java/org/cryptomator/frontend/fuse/AccessPatternIntegrationTest.java @@ -1,64 +1,61 @@ package org.cryptomator.frontend.fuse; -import jnr.ffi.Pointer; -import jnr.ffi.Runtime; -import jnr.ffi.provider.jffi.ByteBufferMemoryIO; +import org.cryptomator.jfuse.api.FileInfo; +import org.cryptomator.jfuse.api.Fuse; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; 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.OS; import org.junit.jupiter.api.io.TempDir; -import org.slf4j.impl.SimpleLogger; -import ru.serce.jnrfuse.FuseException; -import ru.serce.jnrfuse.struct.FuseFileInfo; -import ru.serce.jnrfuse.utils.WinPathUtils; +import org.mockito.Mockito; import java.nio.ByteBuffer; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Arrays; +import java.util.Set; import static java.nio.charset.StandardCharsets.US_ASCII; public class AccessPatternIntegrationTest { static { - System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "debug"); - System.setProperty(SimpleLogger.SHOW_DATE_TIME_KEY, "true"); - System.setProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, "HH:mm:ss.SSS"); + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); + System.setProperty("org.slf4j.simpleLogger.showDateTime", "true"); + System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "HH:mm:ss.SSS"); } private FuseNioAdapter adapter; @BeforeEach void setup(@TempDir Path tmpDir) { - Assumptions.assumeTrue(onWindowsWinFspInstalled(), "WinFSP seem not to be installed."); - adapter = AdapterFactory.createReadWriteAdapter(tmpDir); + var builder = Fuse.builder(); + adapter = ReadWriteAdapter.create(builder.errno(), tmpDir, FuseNioAdapter.DEFAULT_MAX_FILENAMELENGTH, FileNameTranscoder.transcoder()); } @Test @DisplayName("simulate TextEdit.app's access pattern during save") void testAppleAutosaveAccessPattern() { // echo "asd" > foo.txt - FuseFileInfo fi1 = new MockFuseFileInfo(); + FileInfo fi1 = Mockito.spy(new SimpleFileInfo()); adapter.create("/foo.txt", 0644, fi1); - adapter.write("/foo.txt", mockPointer(US_ASCII.encode("asd")), 3, 0, fi1); + adapter.write("/foo.txt", US_ASCII.encode("asd"), 3, 0, fi1); // mkdir foo.txt-temp3000 adapter.mkdir("foo.txt-temp3000", 0755); // echo "asdasd" > foo.txt-temp3000/foo.txt - FuseFileInfo fi2 = new MockFuseFileInfo(); + FileInfo fi2 = Mockito.spy(new SimpleFileInfo()); adapter.create("/foo.txt-temp3000/foo.txt", 0644, fi2); - adapter.write("/foo.txt-temp3000/foo.txt", mockPointer(US_ASCII.encode("asdasd")), 6, 0, fi2); + adapter.write("/foo.txt-temp3000/foo.txt", US_ASCII.encode("asdasd"), 6, 0, fi2); // mv foo.txt foo.txt-temp3001 - adapter.rename("/foo.txt", "/foo.txt-temp3001"); + adapter.rename("/foo.txt", "/foo.txt-temp3001", 0); // mv foo.txt-temp3000/foo.txt foo.txt - adapter.rename("/foo.txt-temp3000/foo.txt", "/foo.txt"); + adapter.rename("/foo.txt-temp3000/foo.txt", "/foo.txt", 0); adapter.release("/foo.txt-temp3000/foo.txt", fi2); // rm -r foo.txt-temp3000 @@ -70,29 +67,21 @@ void testAppleAutosaveAccessPattern() { // cat foo.txt == "asdasd" ByteBuffer buf = ByteBuffer.allocate(7); - FuseFileInfo fi3 = new MockFuseFileInfo(); + FileInfo fi3 = Mockito.spy(new SimpleFileInfo()); adapter.open("/foo.txt", fi3); - int numRead = adapter.read("/foo.txt", mockPointer(buf), 7, 0, fi3); + int numRead = adapter.read("/foo.txt", buf, 7, 0, fi3); adapter.release("/foo.txt", fi3); Assertions.assertEquals(6, numRead); Assertions.assertArrayEquals("asdasd".getBytes(US_ASCII), Arrays.copyOf(buf.array(), numRead)); } - private boolean onWindowsWinFspInstalled() { - try { - return !WinPathUtils.getWinFspPath().isBlank(); - } catch (FuseException e) { - //TODO: log? - return false; - } - } - @Test @DisplayName("create, move and delete symlinks") - @DisabledOnOs(OS.WINDOWS) // Symlinks require either admin privileges or enabled developer mode on windows + @DisabledOnOs(OS.WINDOWS) + // Symlinks require either admin privileges or enabled developer mode on windows void testCreateMoveAndDeleteSymlinks() { // touch foo.txt - FuseFileInfo fi1 = new MockFuseFileInfo(); + FileInfo fi1 = Mockito.mock(FileInfo.class); adapter.create("/foo.txt", 0644, fi1); // ln -s foo.txt bar.txt @@ -111,8 +100,8 @@ void testCreateMoveAndDeleteSymlinks() { assertSymlinkTargetExists("/bar.txt", false); // move both to subdir - adapter.rename("/foo.txt", "/test/foo.txt"); - adapter.rename("/bar.txt", "/test/bar.txt"); + adapter.rename("/foo.txt", "/test/foo.txt", 0); + adapter.rename("/bar.txt", "/test/bar.txt", 0); assertSymlinkTargetExists("/test/bar.txt", false); // delete all @@ -124,7 +113,7 @@ void testCreateMoveAndDeleteSymlinks() { } private void assertSymlinkTargetExists(String symlink, boolean targetIsDirectory) { - FuseFileInfo fi = new MockFuseFileInfo(); + FileInfo fi = Mockito.mock(FileInfo.class); int returnCode = targetIsDirectory ? adapter.opendir(symlink, fi) : adapter.open(symlink, fi); if (returnCode == 0) { int err = targetIsDirectory ? adapter.releasedir(symlink, fi) : adapter.release(symlink, fi); @@ -132,15 +121,33 @@ private void assertSymlinkTargetExists(String symlink, boolean targetIsDirectory Assertions.assertEquals(0, returnCode); } - private static class MockFuseFileInfo extends FuseFileInfo { + private static class SimpleFileInfo implements FileInfo { - public MockFuseFileInfo() { - super(Runtime.getSystemRuntime()); + private long fh; + @Override + public void setFh(long fh) { + this.fh = fh; + } + + @Override + public long getFh() { + return fh; } - } - private Pointer mockPointer(ByteBuffer buf) { - return new ByteBufferMemoryIO(Runtime.getSystemRuntime(), buf); + @Override + public int getFlags() { + return 0; + } + + @Override + public Set getOpenFlags() { + return Set.of(); + } + + @Override + public long getLockOwner() { + return 0; + } } } diff --git a/src/test/java/org/cryptomator/frontend/fuse/BitMaskEnumUtilTest.java b/src/test/java/org/cryptomator/frontend/fuse/BitMaskEnumUtilTest.java deleted file mode 100644 index 35aae35..0000000 --- a/src/test/java/org/cryptomator/frontend/fuse/BitMaskEnumUtilTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.util.EnumSet; -import java.util.Set; -import java.util.stream.Stream; - -import jnr.constants.Constant; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class BitMaskEnumUtilTest { - - @ParameterizedTest - @MethodSource("argumentsProvider") - public void testBitMaskToSet(Set set, long mask) { - BitMaskEnumUtil util = new BitMaskEnumUtil(); - Set actual = util.bitMaskToSet(TestEnum.class, mask); - Assertions.assertEquals(set, actual); - } - - @ParameterizedTest - @MethodSource("argumentsProvider") - public void testSetToBitMask(Set set, long mask) { - BitMaskEnumUtil util = new BitMaskEnumUtil(); - long actual = util.setToBitMask(set); - Assertions.assertEquals(mask, actual); - } - - static Stream argumentsProvider() { - return Stream.of( // - Arguments.of(EnumSet.noneOf(TestEnum.class), 0l), // - Arguments.of(EnumSet.of(TestEnum.ONE), 1l), // - Arguments.of(EnumSet.of(TestEnum.TWO), 2l), // - Arguments.of(EnumSet.of(TestEnum.FOUR), 4l), // - Arguments.of(EnumSet.of(TestEnum.ONE, TestEnum.TWO), 3l), // - Arguments.of(EnumSet.allOf(TestEnum.class), 7l) // - ); - } - - private enum TestEnum implements Constant { - ONE(0x1l), - TWO(0x2l), - FOUR(0x4l); - - private final long value; - TestEnum(long value) { this.value = value; } - public final int intValue() { return (int) value; } - public final long longValue() { return value; } - public final boolean defined() { return true; } - - } -} diff --git a/src/test/java/org/cryptomator/frontend/fuse/FileAttributesUtilTest.java b/src/test/java/org/cryptomator/frontend/fuse/FileAttributesUtilTest.java index 2a83d65..41b1381 100644 --- a/src/test/java/org/cryptomator/frontend/fuse/FileAttributesUtilTest.java +++ b/src/test/java/org/cryptomator/frontend/fuse/FileAttributesUtilTest.java @@ -1,5 +1,14 @@ package org.cryptomator.frontend.fuse; +import org.cryptomator.jfuse.api.Stat; +import org.cryptomator.jfuse.api.TimeSpec; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + import java.nio.file.AccessMode; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -10,43 +19,30 @@ import java.util.Set; import java.util.stream.Stream; -import jnr.posix.util.Platform; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mockito; -import ru.serce.jnrfuse.flags.AccessConstants; -import ru.serce.jnrfuse.struct.FileStat; - public class FileAttributesUtilTest { @ParameterizedTest @MethodSource("accessModeProvider") public void testAccessModeMaskToSet(Set expectedModes, int mask) { - FileAttributesUtil util = new FileAttributesUtil(); - Set accessModes = util.accessModeMaskToSet(mask); + Set accessModes = FileAttributesUtil.accessModeMaskToSet(mask); Assertions.assertEquals(expectedModes, accessModes); } static Stream accessModeProvider() { return Stream.of( // Arguments.of(EnumSet.noneOf(AccessMode.class), 0), // - Arguments.of(EnumSet.of(AccessMode.READ), AccessConstants.R_OK), // - Arguments.of(EnumSet.of(AccessMode.WRITE), AccessConstants.W_OK), // - Arguments.of(EnumSet.of(AccessMode.EXECUTE), AccessConstants.X_OK), // - Arguments.of(EnumSet.of(AccessMode.READ, AccessMode.WRITE), AccessConstants.R_OK | AccessConstants.W_OK), // - Arguments.of(EnumSet.allOf(AccessMode.class), AccessConstants.R_OK | AccessConstants.W_OK | AccessConstants.X_OK | AccessConstants.F_OK) // + Arguments.of(EnumSet.of(AccessMode.READ), 4), // + Arguments.of(EnumSet.of(AccessMode.WRITE), 2), // + Arguments.of(EnumSet.of(AccessMode.EXECUTE), 1), // + Arguments.of(EnumSet.of(AccessMode.READ, AccessMode.WRITE), 4 | 2), // + Arguments.of(EnumSet.allOf(AccessMode.class), 4 | 2 | 1) // ); } @ParameterizedTest @MethodSource("filePermissionProvider") public void testOctalModeToPosixPermissions(Set expectedPerms, long octalMode) { - FileAttributesUtil util = new FileAttributesUtil(); - Set perms = util.octalModeToPosixPermissions(octalMode); + Set perms = FileAttributesUtil.octalModeToPosixPermissions(octalMode); Assertions.assertEquals(expectedPerms, perms); } @@ -72,29 +68,26 @@ public void testCopyBasicFileAttributesFromNioToFuse() { Mockito.when(attr.lastAccessTime()).thenReturn(ftime); Mockito.when(attr.size()).thenReturn(42l); - FileAttributesUtil util = new FileAttributesUtil(); - FileStat stat = new FileStat(jnr.ffi.Runtime.getSystemRuntime()); - util.copyBasicFileAttributesFromNioToFuse(attr, stat); + var stat = Mockito.mock(Stat.class); + var mtime = Mockito.mock(TimeSpec.class); + var atime = Mockito.mock(TimeSpec.class); + var btime = Mockito.mock(TimeSpec.class); + Mockito.doReturn(mtime).when(stat).mTime(); + Mockito.doReturn(atime).when(stat).aTime(); + Mockito.doReturn(btime).when(stat).birthTime(); + FileAttributesUtil.copyBasicFileAttributesFromNioToFuse(attr, stat); - Assertions.assertTrue((FileStat.S_IFDIR & stat.st_mode.intValue()) == FileStat.S_IFDIR); - Assertions.assertEquals(424242l, stat.st_mtim.tv_sec.get()); - Assertions.assertEquals(42, stat.st_mtim.tv_nsec.intValue()); - Assertions.assertEquals(424242l, stat.st_ctim.tv_sec.get()); - Assertions.assertEquals(42, stat.st_ctim.tv_nsec.intValue()); - Assumptions.assumingThat(Platform.IS_MAC || Platform.IS_WINDOWS, () -> { - Assertions.assertEquals(424242l, stat.st_birthtime.tv_sec.get()); - Assertions.assertEquals(42, stat.st_birthtime.tv_nsec.intValue()); - }); - Assertions.assertEquals(424242l, stat.st_atim.tv_sec.get()); - Assertions.assertEquals(42, stat.st_atim.tv_nsec.intValue()); - Assertions.assertEquals(42l, stat.st_size.longValue()); + Mockito.verify(stat).setModeBits(Stat.S_IFDIR); + Mockito.verify(mtime).set(Instant.ofEpochSecond(424242L, 42L)); + Mockito.verify(atime).set(Instant.ofEpochSecond(424242L, 42L)); + Mockito.verify(btime).set(Instant.ofEpochSecond(424242L, 42L)); + Mockito.verify(stat).setSize(42L); } @ParameterizedTest @MethodSource("filePermissionProvider") public void testPosixPermissionsToOctalMode(Set permissions, long expectedMode) { - FileAttributesUtil util = new FileAttributesUtil(); - long mode = util.posixPermissionsToOctalMode(permissions); + long mode = FileAttributesUtil.posixPermissionsToOctalMode(permissions); Assertions.assertEquals(expectedMode, mode); } diff --git a/src/test/java/org/cryptomator/frontend/fuse/mount/MacUtilTest.java b/src/test/java/org/cryptomator/frontend/fuse/MacUtilTest.java similarity index 95% rename from src/test/java/org/cryptomator/frontend/fuse/mount/MacUtilTest.java rename to src/test/java/org/cryptomator/frontend/fuse/MacUtilTest.java index ed6a447..7f790b5 100644 --- a/src/test/java/org/cryptomator/frontend/fuse/mount/MacUtilTest.java +++ b/src/test/java/org/cryptomator/frontend/fuse/MacUtilTest.java @@ -1,4 +1,4 @@ -package org.cryptomator.frontend.fuse.mount; +package org.cryptomator.frontend.fuse; import org.cryptomator.frontend.fuse.MacUtil; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/cryptomator/frontend/fuse/OpenOptionsUtilTest.java b/src/test/java/org/cryptomator/frontend/fuse/OpenOptionsUtilTest.java deleted file mode 100644 index b514c01..0000000 --- a/src/test/java/org/cryptomator/frontend/fuse/OpenOptionsUtilTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.cryptomator.frontend.fuse; - -import java.nio.file.OpenOption; -import java.nio.file.StandardOpenOption; -import java.util.EnumSet; -import java.util.Set; -import java.util.stream.Stream; - -import com.google.common.collect.Sets; -import jnr.constants.platform.OpenFlags; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mockito; - -public class OpenOptionsUtilTest { - - @ParameterizedTest - @MethodSource("openOptionsProvider") - public void testOpenFlagsMaskToSet(Set expectedOptions, Set flags) { - BitMaskEnumUtil enumUtil = Mockito.mock(BitMaskEnumUtil.class); - Mockito.verifyNoMoreInteractions(enumUtil); - OpenOptionsUtil util = new OpenOptionsUtil(enumUtil); - Set options = util.fuseOpenFlagsToNioOpenOptions(flags); - Assertions.assertEquals(expectedOptions, options); - } - - static Stream openOptionsProvider() { - return Stream.of( // - Arguments.of(Sets.newHashSet(StandardOpenOption.READ), EnumSet.of(OpenFlags.O_RDONLY)), // - Arguments.of(Sets.newHashSet(StandardOpenOption.WRITE), EnumSet.of(OpenFlags.O_WRONLY)), // - Arguments.of(Sets.newHashSet(StandardOpenOption.WRITE), EnumSet.of(OpenFlags.O_WRONLY, OpenFlags.O_RDONLY)), // write wins - Arguments.of(Sets.newHashSet(StandardOpenOption.READ, StandardOpenOption.WRITE), EnumSet.of(OpenFlags.O_RDWR)), // - Arguments.of(Sets.newHashSet(StandardOpenOption.READ, StandardOpenOption.WRITE), EnumSet.of(OpenFlags.O_RDWR, OpenFlags.O_WRONLY, OpenFlags.O_RDONLY)), // - Arguments.of(Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING), EnumSet.of(OpenFlags.O_WRONLY, OpenFlags.O_TRUNC)) // - ); - } - -} diff --git a/src/test/java/org/cryptomator/frontend/fuse/locks/LockManagerTest.java b/src/test/java/org/cryptomator/frontend/fuse/locks/LockManagerTest.java index 634f3cf..dc2dad0 100644 --- a/src/test/java/org/cryptomator/frontend/fuse/locks/LockManagerTest.java +++ b/src/test/java/org/cryptomator/frontend/fuse/locks/LockManagerTest.java @@ -11,7 +11,6 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -37,11 +36,11 @@ public void testLockCountDuringLock() { Assertions.assertFalse(lockManager.isPathLocked("/foo")); Assertions.assertFalse(lockManager.isPathLocked("/foo/bar")); Assertions.assertFalse(lockManager.isPathLocked("/foo/bar/baz")); - try (PathLock lock1 = lockManager.createPathLock("/foo/bar/baz").forReading()) { + try (PathLock lock1 = lockManager.lockForReading("/foo/bar/baz")) { Assertions.assertTrue(lockManager.isPathLocked("/foo")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar/baz")); - try (PathLock lock2 = lockManager.createPathLock("/foo/bar/baz").forReading()) { + try (PathLock lock2 = lockManager.lockForReading("/foo/bar/baz")) { Assertions.assertNotSame(lock1, lock2); Assertions.assertTrue(lockManager.isPathLocked("/foo")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar")); @@ -50,7 +49,7 @@ public void testLockCountDuringLock() { Assertions.assertTrue(lockManager.isPathLocked("/foo")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar/baz")); - try (PathLock lock3 = lockManager.createPathLock("/foo/bar/baz").forReading()) { + try (PathLock lock3 = lockManager.lockForReading("/foo/bar/baz")) { Assertions.assertNotSame(lock1, lock3); Assertions.assertTrue(lockManager.isPathLocked("/foo")); Assertions.assertTrue(lockManager.isPathLocked("/foo/bar")); @@ -70,26 +69,23 @@ public void testLockCountDuringLock() { public void testMultipleReadLocks() { LockManager lockManager = new LockManager(); int numThreads = 8; - ExecutorService threadPool = Executors.newFixedThreadPool(numThreads); - CyclicBarrier ready = new CyclicBarrier(8); - CountDownLatch done = new CountDownLatch(numThreads); - - for (int i = 0; i < numThreads; i++) { - int threadnum = i; - threadPool.submit(() -> { - try (PathLock lock = lockManager.createPathLock("/foo/bar/baz").forReading()) { - LOG.trace("ENTER thread {}", threadnum); - ready.await(); - done.countDown(); - LOG.trace("LEAVE thread {}", threadnum); - } catch (InterruptedException | BrokenBarrierException e) { - LOG.error("thread interrupted", e); + CyclicBarrier ready = new CyclicBarrier(numThreads); + + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < numThreads; i++) { + int threadnum = i; + executor.submit(() -> { + try (PathLock lock = lockManager.lockForReading("/foo/bar/baz")) { + LOG.trace("ENTER thread {}", threadnum); + ready.await(); + LOG.trace("LEAVE thread {}", threadnum); + } catch (InterruptedException | BrokenBarrierException e) { + LOG.error("thread interrupted", e); + } + }); } - }); - } - - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10), () -> { // deadlock protection - done.await(); + } }); } @@ -98,99 +94,88 @@ public void testMultipleReadLocks() { public void testMultipleWriteLocks() { LockManager lockManager = new LockManager(); int numThreads = 8; - ExecutorService threadPool = Executors.newFixedThreadPool(numThreads); - CountDownLatch done = new CountDownLatch(numThreads); AtomicBoolean occupied = new AtomicBoolean(false); AtomicBoolean success = new AtomicBoolean(true); - for (int i = 0; i < numThreads; i++) { - int threadnum = i; - threadPool.submit(() -> { - try (PathLock lock = lockManager.createPathLock("/foo/bar/baz").forWriting()) { - LOG.trace("ENTER thread {}", threadnum); - boolean wasFree = occupied.compareAndSet(false, true); - Thread.sleep(50); // give other threads the chance to reach this point - if (!wasFree) { - success.set(false); - } - occupied.set(false); - LOG.trace("LEAVE thread {}", threadnum); - } catch (InterruptedException e) { - LOG.error("thread interrupted", e); + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < numThreads; i++) { + int threadnum = i; + executor.submit(() -> { + try (PathLock lock = lockManager.lockForWriting("/foo/bar/baz")) { + LOG.trace("ENTER thread {}", threadnum); + boolean wasFree = occupied.compareAndSet(false, true); + Thread.sleep(10); // give other threads the chance to reach this point + if (!wasFree) { + success.set(false); + } + occupied.set(false); + LOG.trace("LEAVE thread {}", threadnum); + } catch (InterruptedException e) { + LOG.error("thread interrupted", e); + } + }); } - done.countDown(); - }); - } - - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10), () -> { // deadlock protection - done.await(); + } }); Assertions.assertTrue(success.get()); } @Test - @DisplayName("try-Methods fail with exception if path already locked for writing") - public void testTryForMethodsOnWriteLock() { + @DisplayName("tryLockForWriting() succeeds for unlocked resource") + public void testTryLockForWriting() { LockManager lockManager = new LockManager(); - ExecutorService threadPool = Executors.newFixedThreadPool(1); - CountDownLatch done = new CountDownLatch(1); - AtomicInteger exceptionCounter = new AtomicInteger(); - - try (PathLock lock = lockManager.createPathLock("/foo/bar/baz").forWriting()) { - threadPool.submit(() -> { - try (PathLock lockThread = lockManager.createPathLock("/foo/bar/baz").tryForWriting()) { - //do nuthin' - } catch (AlreadyLockedException e) { - exceptionCounter.incrementAndGet(); - } - try (PathLock lockThread = lockManager.createPathLock("/foo/bar/baz").tryForReading()) { - //do nuthin' - } catch (AlreadyLockedException e) { - exceptionCounter.incrementAndGet(); - } - done.countDown(); - }); - - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { // deadlock protection - done.await(); - }); - } - - Assertions.assertEquals(2, exceptionCounter.get()); + Assertions.assertDoesNotThrow(() -> { + try (PathLock lock = lockManager.tryLockForWriting("/foo/bar/baz")) { + // no-op + } + }); } - @Test - @DisplayName("try-Methods partially fail with exception if path already locked for reading") - public void testTryForMethodsOnReadLock() { + @DisplayName("tryLockForWriting() fails with exception if path already locked for writing") + public void testTryLockForWritingWhenAlreadyWriteLocked() { LockManager lockManager = new LockManager(); - ExecutorService threadPool = Executors.newFixedThreadPool(1); - CountDownLatch done = new CountDownLatch(1); - AtomicInteger exceptionCounter = new AtomicInteger(); + AtomicBoolean exceptionThrown = new AtomicBoolean(); + + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (PathLock existingLock = lockManager.lockForWriting("/foo/bar/baz"); + var executor = Executors.newVirtualThreadPerTaskExecutor()) { + executor.submit(() -> { + try (PathLock lock = lockManager.tryLockForWriting("/foo/bar/baz")) { + exceptionThrown.set(false); + } catch (AlreadyLockedException e) { + exceptionThrown.set(true); + } + }); + } + }); - try (PathLock lock = lockManager.createPathLock("/foo/bar/baz").forReading()) { - threadPool.submit(() -> { - try (PathLock lockThread = lockManager.createPathLock("/foo/bar/baz").tryForWriting()) { - //do nuthin' - } catch (AlreadyLockedException e) { - exceptionCounter.incrementAndGet(); - } + Assertions.assertTrue(exceptionThrown.get()); + } - try (PathLock lockThread = lockManager.createPathLock("/foo/bar/baz").tryForReading()) { - //do nuthin' - } catch (AlreadyLockedException e) { - exceptionCounter.incrementAndGet(); - } - done.countDown(); - }); - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { // deadlock protection - done.await(); - }); - } + @Test + @DisplayName("tryLockForWriting() fails with exception if path already locked for reading") + public void testTryLockForWritingWhenAlreadyReadLocked() { + LockManager lockManager = new LockManager(); + AtomicBoolean exceptionThrown = new AtomicBoolean(); + + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (PathLock existingLock = lockManager.lockForReading("/foo/bar/baz"); + var executor = Executors.newVirtualThreadPerTaskExecutor()) { + executor.submit(() -> { + try (PathLock lock = lockManager.tryLockForWriting("/foo/bar/baz")) { + exceptionThrown.set(false); + } catch (AlreadyLockedException e) { + exceptionThrown.set(true); + } + }); + } + }); - Assertions.assertEquals(1, exceptionCounter.get()); + Assertions.assertTrue(exceptionThrown.get()); } } @@ -204,27 +189,24 @@ class DataLockTests { public void testMultipleReadLocks() { LockManager lockManager = new LockManager(); int numThreads = 8; - ExecutorService threadPool = Executors.newFixedThreadPool(numThreads); - CyclicBarrier ready = new CyclicBarrier(8); - CountDownLatch done = new CountDownLatch(numThreads); - - for (int i = 0; i < numThreads; i++) { - int threadnum = i; - threadPool.submit(() -> { - try (PathLock pathLock = lockManager.createPathLock("/foo/bar/baz").forReading(); // - DataLock dataLock = pathLock.lockDataForReading()) { - LOG.trace("ENTER thread {}", threadnum); - ready.await(); - done.countDown(); - LOG.trace("LEAVE thread {}", threadnum); - } catch (InterruptedException | BrokenBarrierException e) { - LOG.error("thread interrupted", e); + CyclicBarrier ready = new CyclicBarrier(numThreads); + + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < numThreads; i++) { + int threadnum = i; + executor.submit(() -> { + try (PathLock pathLock = lockManager.lockForReading("/foo/bar/baz"); // + DataLock dataLock = pathLock.lockDataForReading()) { + LOG.trace("ENTER thread {}", threadnum); + ready.await(); + LOG.trace("LEAVE thread {}", threadnum); + } catch (InterruptedException | BrokenBarrierException e) { + LOG.error("thread interrupted", e); + } + }); } - }); - } - - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10), () -> { // deadlock protection - done.await(); + } }); } @@ -233,33 +215,30 @@ public void testMultipleReadLocks() { public void testMultipleWriteLocks() { LockManager lockManager = new LockManager(); int numThreads = 8; - ExecutorService threadPool = Executors.newFixedThreadPool(numThreads); - CountDownLatch done = new CountDownLatch(numThreads); AtomicBoolean occupied = new AtomicBoolean(false); AtomicBoolean success = new AtomicBoolean(true); - for (int i = 0; i < numThreads; i++) { - int threadnum = i; - threadPool.submit(() -> { - try (PathLock pathLock = lockManager.createPathLock("/foo/bar/baz").forReading(); // - DataLock dataLock = pathLock.lockDataForWriting()) { - LOG.trace("ENTER thread {}", threadnum); - boolean wasFree = occupied.compareAndSet(false, true); - Thread.sleep(50); // give other threads the chance to reach this point - if (!wasFree) { - success.set(false); - } - occupied.set(false); - LOG.trace("LEAVE thread {}", threadnum); - } catch (InterruptedException e) { - LOG.error("thread interrupted", e); + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { // deadlock protection + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < numThreads; i++) { + int threadnum = i; + executor.submit(() -> { + try (PathLock pathLock = lockManager.lockForReading("/foo/bar/baz"); // + DataLock dataLock = pathLock.lockDataForWriting()) { + LOG.trace("ENTER thread {}", threadnum); + boolean wasFree = occupied.compareAndSet(false, true); + Thread.sleep(10); // give other threads the chance to reach this point + if (!wasFree) { + success.set(false); + } + occupied.set(false); + LOG.trace("LEAVE thread {}", threadnum); + } catch (InterruptedException e) { + LOG.error("thread interrupted", e); + } + }); } - done.countDown(); - }); - } - - Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10), () -> { // deadlock protection - done.await(); + } }); Assertions.assertTrue(success.get()); } diff --git a/src/test/java/org/cryptomator/frontend/fuse/mount/AwtFrameworkRevealer.java b/src/test/java/org/cryptomator/frontend/fuse/mount/AwtFrameworkRevealer.java deleted file mode 100644 index ae43d99..0000000 --- a/src/test/java/org/cryptomator/frontend/fuse/mount/AwtFrameworkRevealer.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import java.awt.Desktop; -import java.io.IOException; -import java.nio.file.Path; - -public class AwtFrameworkRevealer implements Revealer { - - @Override - public void reveal(Path path) throws IOException, UnsupportedOperationException { - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { - Desktop.getDesktop().open(path.toFile()); - } else { - throw new UnsupportedOperationException("Desktop API to browse files not supported."); - } - } - -} diff --git a/src/test/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariablesTest.java b/src/test/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariablesTest.java deleted file mode 100644 index c4e61c3..0000000 --- a/src/test/java/org/cryptomator/frontend/fuse/mount/EnvironmentVariablesTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import org.cryptomator.frontend.fuse.FileNameTranscoder; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; -import java.nio.file.Paths; - -public class EnvironmentVariablesTest { - - @Test - public void testEnvironmentVariablesBuilder() { - String[] flags = new String[]{"--test", "--debug"}; - FileNameTranscoder transcoder = FileNameTranscoder.transcoder(); - Path mountPoint = Paths.get("/home/testuser/mnt"); - - EnvironmentVariables envVars = EnvironmentVariables.create().withFlags(flags).withFileNameTranscoder(transcoder).withMountPoint(mountPoint).build(); - - Assertions.assertEquals(flags, envVars.getFuseFlags()); - Assertions.assertEquals(transcoder, envVars.getFileNameTranscoder()); - Assertions.assertEquals(mountPoint, envVars.getMountPoint()); - } -} diff --git a/src/test/java/org/cryptomator/frontend/fuse/mount/LinuxEnvironmentTest.java b/src/test/java/org/cryptomator/frontend/fuse/mount/LinuxEnvironmentTest.java deleted file mode 100644 index fecd275..0000000 --- a/src/test/java/org/cryptomator/frontend/fuse/mount/LinuxEnvironmentTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.frontend.fuse.mount; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class LinuxEnvironmentTest { - - public static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().contains("linux"); - - public static void main(String[] args) throws IOException { - if (IS_LINUX) { - Path mountPoint = Files.createTempDirectory("fuse-mount"); - Mounter mounter = FuseMountFactory.getMounter(); - EnvironmentVariables envVars = EnvironmentVariables.create() - .withFlags(mounter.defaultMountFlags()) - .withMountPoint(mountPoint) - .build(); - Path tmp = Paths.get("/tmp"); - try (Mount mnt = mounter.mount(tmp, envVars)) { - try { - mnt.reveal(new AwtFrameworkRevealer()); - } catch (Exception e) { - System.out.println("Reveal failed."); - } - System.out.println("Wait for it..."); - System.in.read(); - mnt.unmountForced(); - } catch (IOException | FuseMountException e) { - e.printStackTrace(); - } - } else { - System.out.print("Sorry, this test is only for Linux."); - } - } - -} diff --git a/src/test/java/org/cryptomator/frontend/fuse/mount/MirroringFuseMountTest.java b/src/test/java/org/cryptomator/frontend/fuse/mount/MirroringFuseMountTest.java index 48b3f89..2fb2873 100644 --- a/src/test/java/org/cryptomator/frontend/fuse/mount/MirroringFuseMountTest.java +++ b/src/test/java/org/cryptomator/frontend/fuse/mount/MirroringFuseMountTest.java @@ -5,72 +5,68 @@ import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.DirStructure; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.integrations.mount.MountCapability; +import org.cryptomator.integrations.mount.MountFailedException; +import org.cryptomator.integrations.mount.MountService; +import org.cryptomator.integrations.mount.UnmountFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.impl.SimpleLogger; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.FileSystem; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Scanner; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +/** + * Test programs to mirror an existing directory or vault. + *

+ * Run with {@code --enable-native-access=...} + */ public class MirroringFuseMountTest { static { - System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "debug"); - System.setProperty(SimpleLogger.SHOW_DATE_TIME_KEY, "true"); - System.setProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, "HH:mm:ss.SSS"); + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); + System.setProperty("org.slf4j.simpleLogger.showDateTime", "true"); + System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "HH:mm:ss.SSS"); } private static final Logger LOG = LoggerFactory.getLogger(MirroringFuseMountTest.class); - private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); /** - * Mirror directory on Windows + * Mirror directory */ - public static class WindowsMirror { - - public static void main(String[] args) { - Preconditions.checkState(OS_NAME.contains("win"), "Test designed to run on Windows."); + public static class Mirror { + public static void main(String[] args) throws MountFailedException { + var mountService = MountService.get().findAny().orElseThrow(() -> new MountFailedException("Did not find a mount provider")); + LOG.info("Using mount provider: {}", mountService.displayName()); try (Scanner scanner = new Scanner(System.in)) { System.out.println("Enter path to the directory you want to mirror:"); Path p = Paths.get(scanner.nextLine()); - System.out.println("Enter mount point:"); - Path m = Paths.get(scanner.nextLine()); - if (m.startsWith(p) || p.startsWith(m)) { - LOG.error("Mirrored directory and mount location must not be nested."); - } else if (Files.isDirectory(p)) { - mount(p, m); - } else { - LOG.error("Invalid directory."); - } + mount(mountService, p, scanner); } } + } /** - * Mirror vault on Windows + * Mirror vault */ - public static class WindowsCryptoFsMirror { - - public static void main(String args[]) throws IOException, NoSuchAlgorithmException { - Preconditions.checkState(OS_NAME.contains("win"), "Test designed to run on Windows."); + public static class CryptoFsMirror { + public static void main(String[] args) throws IOException, NoSuchAlgorithmException, MountFailedException { + var mountService = MountService.get().findAny().orElseThrow(() -> new MountFailedException("Did not find a mount provider")); + LOG.info("Using mount provider: {}", mountService.displayName()); try (Scanner scanner = new Scanner(System.in)) { - System.out.println("Enter path to the vault you want to mirror:"); + LOG.info("Enter path to the vault you want to mirror:"); Path vaultPath = Paths.get(scanner.nextLine()); Preconditions.checkArgument(CryptoFileSystemProvider.checkDirStructureForVault(vaultPath, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT, "Not a vault: " + vaultPath); - System.out.println("Enter vault password:"); + LOG.info("Enter vault password:"); String passphrase = scanner.nextLine(); SecureRandom csprng = SecureRandom.getInstanceStrong(); @@ -79,168 +75,54 @@ public static void main(String args[]) throws IOException, NoSuchAlgorithmExcept .build(); try (FileSystem cryptoFs = CryptoFileSystemProvider.newFileSystem(vaultPath, props)) { Path p = cryptoFs.getPath("/"); - System.out.println("Enter mount point:"); - Path m = Paths.get(scanner.nextLine()); - //Preconditions.checkArgument(Files.isDirectory(m), "Invalid mount point: " + m); //We don't need that on Windows - LOG.info("Mounting FUSE file system at {}", m); - mount(p, m); + mount(mountService, p, scanner); } } } } - /** - * Mirror directory on Linux - */ - public static class LinuxMirror { - - public static void main(String args[]) { - Preconditions.checkState(OS_NAME.contains("linux"), "Test designed to run on Linux."); + private static void mount(MountService mountProvider, Path pathToMirror, Scanner scanner) throws MountFailedException { - try (Scanner scanner = new Scanner(System.in)) { - System.out.println("Enter path to the directory you want to mirror:"); - Path p = Paths.get(scanner.nextLine()); - System.out.println("Enter mount point:"); - Path m = Paths.get(scanner.nextLine()); - if (m.startsWith(p) || p.startsWith(m)) { - LOG.error("Mirrored directory and mount location must not be nested."); - } else if (Files.isDirectory(p)) { - mount(p, m); - } else { - LOG.error("Invalid directory."); - } - } + var mountBuilder = mountProvider.forFileSystem(pathToMirror); + if (mountProvider.hasCapability(MountCapability.MOUNT_FLAGS)) { + mountBuilder.setMountFlags(mountProvider.getDefaultMountFlags()); } - - } - - /** - * Mirror vault on Linux - */ - public static class LinuxCryptoFsMirror { - - public static void main(String args[]) throws IOException, NoSuchAlgorithmException { - Preconditions.checkState(OS_NAME.contains("linux"), "Test designed to run on Linux."); - - try (Scanner scanner = new Scanner(System.in)) { - System.out.println("Enter path to the vault you want to mirror:"); - Path vaultPath = Paths.get(scanner.nextLine()); - Preconditions.checkArgument(CryptoFileSystemProvider.checkDirStructureForVault(vaultPath, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT, "Not a vault: " + vaultPath); - - System.out.println("Enter vault password:"); - String passphrase = scanner.nextLine(); - - SecureRandom csprng = SecureRandom.getInstanceStrong(); - CryptoFileSystemProperties props = CryptoFileSystemProperties.cryptoFileSystemProperties() - .withKeyLoader(url -> new MasterkeyFileAccess(new byte[0], csprng).load(vaultPath.resolve("masterkey.cryptomator"), passphrase)) - .build(); - try (FileSystem cryptoFs = CryptoFileSystemProvider.newFileSystem(vaultPath, props)) { - Path p = cryptoFs.getPath("/"); - System.out.println("Enter mount point:"); - Path m = Paths.get(scanner.nextLine()); - Preconditions.checkArgument(Files.isDirectory(m), "Invalid mount point: " + m); - LOG.info("Mounting FUSE file system at {}", m); - mount(p, m); - } - } + if (mountProvider.hasCapability(MountCapability.VOLUME_ID)) { + mountBuilder.setVolumeId("mirror"); } - - } - - /** - * Mirror directory on macOS - */ - public static class MacMirror { - - public static void main(String args[]) { - Preconditions.checkState(OS_NAME.contains("mac"), "Test designed to run on macOS."); - - try (Scanner scanner = new Scanner(System.in)) { - System.out.println("Enter path to the directory you want to mirror:"); - Path p = Paths.get(scanner.nextLine()); - Path m = Paths.get("/Volumes/" + UUID.randomUUID().toString()); - LOG.info("Mounting FUSE file system at {}", m); - mount(p, m); - } + if (mountProvider.hasCapability(MountCapability.VOLUME_NAME)) { + mountBuilder.setVolumeName("Mirror"); } - - } - - /** - * Mirror vault on macOS - */ - public static class MacCryptoFsMirror { - - public static void main(String args[]) throws IOException, NoSuchAlgorithmException { - Preconditions.checkState(OS_NAME.contains("mac"), "Test designed to run on macOS."); - - try (Scanner scanner = new Scanner(System.in)) { - System.out.println("Enter path to the vault you want to mirror:"); - Path vaultPath = Paths.get(scanner.nextLine()); - Preconditions.checkArgument(CryptoFileSystemProvider.checkDirStructureForVault(vaultPath, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT, "Not a vault: " + vaultPath); - - System.out.println("Enter vault password:"); - String passphrase = scanner.nextLine(); - - SecureRandom csprng = SecureRandom.getInstanceStrong(); - CryptoFileSystemProperties props = CryptoFileSystemProperties.cryptoFileSystemProperties() - .withKeyLoader(url -> new MasterkeyFileAccess(new byte[0], csprng).load(vaultPath.resolve("masterkey.cryptomator"), passphrase)) - .build(); - try (FileSystem cryptoFs = CryptoFileSystemProvider.newFileSystem(vaultPath, props)) { - Path p = cryptoFs.getPath("/"); - Path m = Paths.get("/Volumes/" + UUID.randomUUID().toString()); - LOG.info("Mounting FUSE file system at {}", m); - mount(p, m); - } - } + if (mountProvider.hasCapability(MountCapability.LOOPBACK_HOST_NAME)) { + mountBuilder.setLoopbackHostName("mirrorHost"); + } + if (mountProvider.hasCapability(MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH)) { + // don't set a mount point + } else { + LOG.info("Enter mount point: "); + Path m = Paths.get(scanner.nextLine()); + mountBuilder.setMountpoint(m); } - } - - private static void mount(Path pathToMirror, Path mountPoint) { - Mounter mounter = FuseMountFactory.getMounter(); - EnvironmentVariables envVars = EnvironmentVariables.create() - .withFlags(mounter.defaultMountFlags()) - .withMountPoint(mountPoint) - .withFileNameTranscoder(mounter.defaultFileNameTranscoder()) - .build(); - CountDownLatch barrier = new CountDownLatch(1); - Consumer onFuseMainExit = throwable -> barrier.countDown(); - try (Mount mnt = mounter.mount(pathToMirror, envVars, onFuseMainExit, false)) { - LOG.info("Mounted successfully. Enter anything to stop the server..."); - try { - mnt.reveal(new AwtFrameworkRevealer()); - } catch (Exception e) { - LOG.warn("Reveal failed.", e); - } + try (var mount = mountBuilder.mount()) { + LOG.info("Mounted successfully to: {}", mount.getMountpoint().uri()); + LOG.info("Enter anything to unmount..."); System.in.read(); - try { - mnt.unmount(); - } catch (FuseMountException e) { - LOG.info("Unable to perform regular unmount.", e); - LOG.info("Forcing unmount..."); - mnt.unmountForced(); - } - LOG.info("Unmounted successfully. Exiting..."); - } catch (IOException | FuseMountException e) { - LOG.error("Mount failed", e); - } - try { - if (!barrier.await(5000, TimeUnit.MILLISECONDS)) { - LOG.error("Wait on onFuseExit action to finish exceeded timeout. Exiting ..."); - } else { - LOG.info("onExit action executed."); + try { + mount.unmount(); + } catch (UnmountFailedException e) { + if (mountProvider.hasCapability(MountCapability.UNMOUNT_FORCED)) { + LOG.warn("Graceful unmount failed. Attempting force-unmount..."); + mount.unmountForced(); + } } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.error("Main thread interrupted. Exiting without waiting for onFuseExit action"); + } catch (UnmountFailedException e) { + LOG.warn("Unmount failed.", e); + } catch (IOException e) { + throw new UncheckedIOException(e); } } - - private static boolean isVaultDir(Path vaultPath) throws IOException { - return CryptoFileSystemProvider.checkDirStructureForVault(vaultPath, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT; - } } diff --git a/suppression.xml b/suppression.xml index e855d9d..bb67725 100644 --- a/suppression.xml +++ b/suppression.xml @@ -2,8 +2,8 @@ - - ^com\.github\.serceman:jnr-fuse:.*$ + + ^org\.cryptomator:jfuse.*$