diff --git a/.travis.yml b/.travis.yml index 3016c1cb8..b2845a704 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: global: - secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls - secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity + - secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip" @@ -42,7 +43,7 @@ addons: branch_pattern: release.* deploy: - provider: releases +- provider: releases prerelease: false api_key: secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=" @@ -53,3 +54,13 @@ deploy: on: repo: cryptomator/cryptomator tags: true +- provider: script + script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions" + on: + repo: cryptomator/cryptomator + tags: true +- provider: script + script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions" + on: + repo: cryptomator/cryptomator + tags: true diff --git a/README.md b/README.md index fe4ff3890..1b79ec1bf 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator) [![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade) [![Coverage Status](https://coveralls.io/repos/github/cryptomator/cryptomator/badge.svg?branch=master)](https://coveralls.io/github/cryptomator/cryptomator?branch=master) [![Join the chat at https://gitter.im/cryptomator/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator) diff --git a/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index 94a3c9e12..9745ea3df 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -8,7 +8,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 ant-kit pom diff --git a/main/ant-kit/src/main/resources/package/linux/control b/main/ant-kit/src/main/resources/package/linux/control index 91a49094a..95d2a7148 100644 --- a/main/ant-kit/src/main/resources/package/linux/control +++ b/main/ant-kit/src/main/resources/package/linux/control @@ -9,7 +9,7 @@ Priority: optional Architecture: APPLICATION_ARCH Provides: APPLICATION_PACKAGE Installed-Size: APPLICATION_INSTALLED_SIZE -Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils +Depends: gvfs-bin, gvfs-backends, gvfs-fuse Description: Multi-platform client-side encryption of your cloud files. Cryptomator provides free client-side AES encryption for your cloud files. Create encrypted vaults, which get mounted as virtual volumes. Whatever diff --git a/main/commons-test/pom.xml b/main/commons-test/pom.xml index 57d835268..4cfb26495 100644 --- a/main/commons-test/pom.xml +++ b/main/commons-test/pom.xml @@ -10,7 +10,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 commons-test Cryptomator common test dependencies diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 227a83c8b..e0d30d868 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -10,7 +10,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 commons Cryptomator common diff --git a/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java index 649a2b6b6..242c48572 100644 --- a/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java +++ b/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java @@ -1,7 +1,7 @@ package org.cryptomator.common; @FunctionalInterface -public interface ConsumerThrowingException { +public interface ConsumerThrowingException { void accept(T t) throws E; diff --git a/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java index ec453b886..6af091bf1 100644 --- a/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java +++ b/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java @@ -1,7 +1,7 @@ package org.cryptomator.common; @FunctionalInterface -public interface RunnableThrowingException { +public interface RunnableThrowingException { void run() throws T; diff --git a/main/commons/src/main/java/org/cryptomator/common/StackTrace.java b/main/commons/src/main/java/org/cryptomator/common/StackTrace.java new file mode 100644 index 000000000..8d7b64c8a --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/StackTrace.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2016 Markus Kreusch and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch - initial implementation + *******************************************************************************/ +package org.cryptomator.common; + +import java.util.stream.Stream; + +/** + * Utility to print stack traces while analyzing issues. + * + * @author Markus Kreusch + */ +public class StackTrace { + + public static void print(String message) { + Thread thread = Thread.currentThread(); + System.err.println(stackTraceFor(message, thread)); + } + + private static String stackTraceFor(String message, Thread thread) { + StringBuilder result = new StringBuilder(); + appendMessageAndThreadName(result, message, thread); + appendStackTrace(thread, result); + return result.toString(); + } + + private static void appendStackTrace(Thread thread, StringBuilder result) { + Stream.of(thread.getStackTrace()) // + .skip(4) // + .forEach(stackTraceElement -> append(stackTraceElement, result)); + } + + private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) { + result // + .append('[') // + .append(thread.getName()) // + .append("] ") // + .append(message); + } + + private static void append(StackTraceElement stackTraceElement, StringBuilder result) { + String className = stackTraceElement.getClassName(); + String methodName = stackTraceElement.getMethodName(); + String fileName = stackTraceElement.getFileName(); + int lineNumber = stackTraceElement.getLineNumber(); + result.append('\n') // + .append(className).append(':').append(methodName) // + .append(" (").append(fileName).append(':').append(lineNumber).append(')'); + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java new file mode 100644 index 000000000..ed73a51d0 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java @@ -0,0 +1,8 @@ +package org.cryptomator.common; + +@FunctionalInterface +public interface SupplierThrowingException { + + T get() throws E; + +} diff --git a/main/filesystem-api/pom.xml b/main/filesystem-api/pom.xml index 8cf1de3e7..1de71aa2b 100644 --- a/main/filesystem-api/pom.xml +++ b/main/filesystem-api/pom.xml @@ -9,7 +9,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-api Cryptomator filesystem: API diff --git a/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java index 6152f65cb..32e5adac7 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.Reader; import java.io.UncheckedIOException; import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -28,7 +29,9 @@ public final class FileContents { * @return The file's content interpreted in this FileContents' charset. */ public String readContents(File file) { - try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) { + try ( // + ReadableByteChannel channel = file.openReadable(); // + Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) { return IOUtils.toString(reader); } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/main/filesystem-charsets/pom.xml b/main/filesystem-charsets/pom.xml index 38bac29b9..6775acc44 100644 --- a/main/filesystem-charsets/pom.xml +++ b/main/filesystem-charsets/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-charsets Cryptomator filesystem: Charset compatibility layer diff --git a/main/filesystem-crypto-integration-tests/pom.xml b/main/filesystem-crypto-integration-tests/pom.xml index f07655191..fec263561 100644 --- a/main/filesystem-crypto-integration-tests/pom.xml +++ b/main/filesystem-crypto-integration-tests/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-crypto-integration-tests Cryptomator filesystem: Encryption layer tests diff --git a/main/filesystem-crypto/pom.xml b/main/filesystem-crypto/pom.xml index 8b55b4b52..cff7e46ca 100644 --- a/main/filesystem-crypto/pom.xml +++ b/main/filesystem-crypto/pom.xml @@ -12,14 +12,14 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-crypto Cryptomator filesystem: Encryption layer 1.51 - 1.0.4 + 1.0.7 diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java index 6833d1b9a..75408f609 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -27,7 +27,7 @@ class FilenameCryptorImpl implements FilenameCryptor { private static final BaseNCodec BASE32 = new Base32(); // https://tools.ietf.org/html/rfc4648#section-6 - private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z2-7]{8})*[A-Z2-7=]{8}"); + private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}"); private static final ThreadLocal SHA1 = new ThreadLocalSha1(); private static final ThreadLocal AES_SIV = new ThreadLocal() { @Override diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java index 6b8a1f428..138ec226f 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java @@ -85,11 +85,15 @@ final class ConflictResolver { } else { ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize); ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize); - r1.read(beginOfFile1); - r2.read(beginOfFile2); - beginOfFile1.flip(); - beginOfFile2.flip(); - return beginOfFile1.equals(beginOfFile2); + int bytesRead1 = r1.read(beginOfFile1); + int bytesRead2 = r2.read(beginOfFile2); + if (bytesRead1 == bytesRead2) { + beginOfFile1.flip(); + beginOfFile2.flip(); + return beginOfFile1.equals(beginOfFile2); + } else { + return false; + } } } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index b66721ca2..af9698d12 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -10,6 +10,7 @@ package org.cryptomator.filesystem.crypto; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.lang3.StringUtils.removeStart; import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX; import java.io.FileNotFoundException; @@ -91,15 +92,15 @@ class CryptoFolder extends CryptoNode implements Folder { private Stream nonConflictingFiles() { if (exists()) { final Stream files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty()); - return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct(); + return files.filter(startsWithEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct(); } else { throw new UncheckedIOException(new FileNotFoundException(format("Folder %s does not exist", this))); } } - private Predicate containsEncryptedName() { + private Predicate startsWithEncryptedName() { final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern(); - return (File file) -> encryptedNamePattern.matcher(file.name()).find(); + return (File file) -> encryptedNamePattern.matcher(removeStart(file.name(),DIR_PREFIX)).find(); } Optional decryptChildName(String ciphertextFileName) { diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java index c58c31384..9e8578dc2 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java @@ -16,6 +16,7 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import javax.inject.Inject; import javax.inject.Provider; @@ -52,10 +53,14 @@ class Masterkeys { public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException { File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME); Cryptor cryptor = cryptorProvider.get(); + boolean success = false; try { readMasterKey(masterkeyFile, cryptor, passphrase); - } catch (UncheckedIOException e) { - cryptor.destroy(); + success = true; + } finally { + if (!success) { + cryptor.destroy(); + } } return cryptor; } @@ -86,7 +91,9 @@ class Masterkeys { /* I/O */ private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException { - try (InputStream in = Channels.newInputStream(file.openReadable())) { + try ( // + ReadableByteChannel channel = file.openReadable(); // + InputStream in = Channels.newInputStream(channel)) { final byte[] fileContents = IOUtils.toByteArray(in); cryptor.readKeysFromMasterkeyFile(fileContents, passphrase); } catch (IOException e) { diff --git a/main/filesystem-inmemory/pom.xml b/main/filesystem-inmemory/pom.xml index 272df5d6b..f015a33d1 100644 --- a/main/filesystem-inmemory/pom.xml +++ b/main/filesystem-inmemory/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-inmemory Cryptomator filesystem: In-memory mock diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java index 6ba8d326b..90eab31f3 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java @@ -64,8 +64,10 @@ class InMemoryReadableFile implements ReadableFile { @Override public void close() throws UncheckedIOException { - open.set(false); - readLock.unlock(); + if (open.get()) { + open.set(false); + readLock.unlock(); + } } } diff --git a/main/filesystem-invariants-tests/pom.xml b/main/filesystem-invariants-tests/pom.xml index e8ebda642..7fa900479 100644 --- a/main/filesystem-invariants-tests/pom.xml +++ b/main/filesystem-invariants-tests/pom.xml @@ -9,7 +9,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-invariants-tests Cryptomator filesystem: Invariants tests diff --git a/main/filesystem-nameshortening/pom.xml b/main/filesystem-nameshortening/pom.xml index 254611fe4..e81a2b4b7 100644 --- a/main/filesystem-nameshortening/pom.xml +++ b/main/filesystem-nameshortening/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-nameshortening Cryptomator filesystem: Name shortening layer diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java index ac8b056fe..ef5532767 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java @@ -14,7 +14,7 @@ final class ConflictResolver { private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class); private static final String LONG_NAME_FILE_EXT = ".lng"; - private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}"); + private static final Pattern BASE32_PATTERN = Pattern.compile("^0?([A-Z2-7]{8})*[A-Z2-7=]{8}"); private static final int UUID_FIRST_GROUP_STRLEN = 8; private ConflictResolver() { diff --git a/main/filesystem-nio/pom.xml b/main/filesystem-nio/pom.xml index d4883537f..ff97f717a 100644 --- a/main/filesystem-nio/pom.xml +++ b/main/filesystem-nio/pom.xml @@ -7,7 +7,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-nio Cryptomator filesystem: NIO-based physical layer diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java index 5d61ccb89..2196b7a3e 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java @@ -2,6 +2,7 @@ package org.cryptomator.filesystem.nio; import java.io.IOException; import java.nio.channels.AsynchronousFileChannel; +import java.nio.file.AccessDeniedException; import java.nio.file.CopyOption; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -53,7 +54,17 @@ class DefaultNioAccess implements NioAccess { @Override public void delete(Path path) throws IOException { - Files.delete(path); + try { + Files.delete(path); + } catch (AccessDeniedException e) { + // workaround for https://github.com/cryptomator/cryptomator/issues/317 + try { + if (path.toFile().delete()) return; + } catch (UnsupportedOperationException e2) { + // ignore + } + throw e; + } } @Override diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java index 5e38a8e25..e1afc2718 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java @@ -30,13 +30,21 @@ class NioFile extends NioNode implements File { @Override public ReadableFile openReadable() throws UncheckedIOException { if (lock.getWriteHoldCount() > 0) { - throw new IllegalStateException("Current thread is currently writing this file"); + throw new IllegalStateException("Current thread is currently writing " + path); } if (lock.getReadHoldCount() > 0) { - throw new IllegalStateException("Current thread is already reading this file"); + throw new IllegalStateException("Current thread is already reading " + path); } lock.readLock().lock(); - return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock); + ReadableFile result = null; + try { + result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock); + } finally { + if (result == null) { + unlockReadLock(); + } + } + return result; } private void unlockReadLock() { @@ -46,13 +54,21 @@ class NioFile extends NioNode implements File { @Override public WritableFile openWritable() throws UncheckedIOException { if (lock.getWriteHoldCount() > 0) { - throw new IllegalStateException("Current thread is already writing this file"); + throw new IllegalStateException("Current thread is already writing " + path); } if (lock.getReadHoldCount() > 0) { - throw new IllegalStateException("Current thread is currently reading this file"); + throw new IllegalStateException("Current thread is currently reading " + path); } lockWriteLock(); - return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock); + WritableFile result = null; + try { + result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock); + } finally { + if (result == null) { + unlockWriteLock(); + } + } + return result; } // visible for testing diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java index 489849f24..ec4269fc9 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java @@ -99,10 +99,11 @@ public class NioFileTest { @Test public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() { + when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class)); inTest.openReadable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("already reading this file"); + thrown.expectMessage("already reading " + path); inTest.openReadable(); } @@ -111,7 +112,7 @@ public class NioFileTest { public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() { ReadableNioFile readableNioFile = mock(ReadableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile); + when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile); inTest.openReadable(); captor.getValue().run(); @@ -122,10 +123,11 @@ public class NioFileTest { @Test public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() { + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("currently writing this file"); + thrown.expectMessage("currently writing " + path); inTest.openReadable(); } @@ -133,7 +135,7 @@ public class NioFileTest { @Test public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() { ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null); + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); captor.getValue().run(); @@ -154,7 +156,7 @@ public class NioFileTest { public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() { WritableNioFile writableNioFile = mock(WritableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile); + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile); inTest.openWritable(); captor.getValue().run(); @@ -165,28 +167,31 @@ public class NioFileTest { @Test public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() { + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("already writing this file"); + thrown.expectMessage("already writing " + path); inTest.openWritable(); } @Test public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() { + when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class)); inTest.openReadable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("currently reading this file"); + thrown.expectMessage("currently reading " + path); inTest.openWritable(); } @Test public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() { + ReadableNioFile readableNioFile = mock(ReadableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null); + when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile); inTest.openReadable(); captor.getValue().run(); diff --git a/main/filesystem-stats/pom.xml b/main/filesystem-stats/pom.xml index 3a0ec4b41..db78fccd0 100644 --- a/main/filesystem-stats/pom.xml +++ b/main/filesystem-stats/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 filesystem-stats Cryptomator filesystem: Throughput statistics diff --git a/main/frontend-api/pom.xml b/main/frontend-api/pom.xml index d28511515..7eedb470e 100644 --- a/main/frontend-api/pom.xml +++ b/main/frontend-api/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 frontend-api Cryptomator frontend: API diff --git a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java index 2be80c778..78978bf7b 100644 --- a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java +++ b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java @@ -14,12 +14,20 @@ import java.util.Optional; public interface Frontend extends AutoCloseable { public enum MountParam { - MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER + MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER, + + /** + * "dav" or "webdav" + */ + PREFERRED_GVFS_SCHEME } void mount(Map> map) throws CommandFailedException; - void unmount() throws CommandFailedException; + /** + * Unmounts the file system and stops any file system handler threads. + */ + void close() throws Exception; void reveal() throws CommandFailedException; diff --git a/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendFactory.java b/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendFactory.java index 9401355d0..37fb053cd 100644 --- a/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendFactory.java +++ b/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendFactory.java @@ -16,10 +16,11 @@ public interface FrontendFactory { * Provides a new frontend to access the given folder. * * @param root Root resource accessible through this frontend. - * @param uniqueName Name of the frontend, i.e. used to create subresources for the different frontends inside of a common virtual drive. + * @param id unique id of the frontend, i.e. used to generate a unique uri + * @param name Name of the frontend, i.e. used to generate a readable/recognizable name of a common virtual drive * @return A new frontend * @throws FrontendCreationFailedException If creation was not possible. */ - Frontend create(Folder root, String uniqueName) throws FrontendCreationFailedException; + Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException; } diff --git a/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendId.java b/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendId.java new file mode 100644 index 000000000..d5608bdac --- /dev/null +++ b/main/frontend-api/src/main/java/org/cryptomator/frontend/FrontendId.java @@ -0,0 +1,85 @@ +package org.cryptomator.frontend; + +import static java.util.UUID.randomUUID; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.UUID; + +public class FrontendId implements Serializable { + + public static final String FRONTEND_ID_PATTERN = "[a-zA-Z0-9_-]{12}"; + + public static FrontendId generate() { + return new FrontendId(); + } + + public static FrontendId from(String value) { + return new FrontendId(value); + } + + private final String value; + + private FrontendId() { + this(generateId()); + } + + private FrontendId(String value) { + if (!value.matches(FRONTEND_ID_PATTERN)) { + throw new IllegalArgumentException("Invalid frontend id " + value); + } + this.value = value; + } + + private static String generateId() { + return asBase64String(nineBytesFrom(randomUUID())); + } + + private static String asBase64String(ByteBuffer bytes) { + ByteBuffer base64Buffer = Base64.getUrlEncoder().encode(bytes); + return new String(asByteArray(base64Buffer), StandardCharsets.US_ASCII); + } + + private static ByteBuffer nineBytesFrom(UUID uuid) { + ByteBuffer uuidBuffer = ByteBuffer.allocate(9); + uuidBuffer.putLong(uuid.getMostSignificantBits()); + uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF)); + uuidBuffer.flip(); + return uuidBuffer; + } + + private static byte[] asByteArray(ByteBuffer buffer) { + if (buffer.hasArray()) { + return buffer.array(); + } else { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return bytes; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + return obj == this || internalEquals((FrontendId) obj); + } + + private boolean internalEquals(FrontendId obj) { + return value.equals(obj.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value; + } + +} diff --git a/main/frontend-webdav/pom.xml b/main/frontend-webdav/pom.xml index d1178790d..77f9d8efa 100644 --- a/main/frontend-webdav/pom.xml +++ b/main/frontend-webdav/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 frontend-webdav Cryptomator frontend: WebDAV frontend diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/ContextPathBuilder.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/ContextPathBuilder.java new file mode 100644 index 000000000..ee036d221 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/ContextPathBuilder.java @@ -0,0 +1,30 @@ +package org.cryptomator.frontend.webdav; + +import static java.lang.String.format; +import static org.cryptomator.frontend.FrontendId.FRONTEND_ID_PATTERN; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.cryptomator.frontend.FrontendId; + +class ContextPaths { + + private static final Pattern SERVLET_PATH_WITH_FRONTEND_ID_PATTERN = Pattern.compile("^/(" + FRONTEND_ID_PATTERN + ")(/.*)?$"); + private static final int FRONTEND_ID_GROUP = 1; + + public static String from(FrontendId id, String name) { + return format("/%s/%s", id, name); + } + + public static Optional extractFrontendId(String path) { + Matcher matcher = SERVLET_PATH_WITH_FRONTEND_ID_PATTERN.matcher(path); + if (matcher.matches()) { + return Optional.of(FrontendId.from(matcher.group(FRONTEND_ID_GROUP))); + } else { + return Optional.empty(); + } + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/DefaultServlet.java similarity index 70% rename from main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java rename to main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/DefaultServlet.java index 0edfc92f8..dfd74a572 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/DefaultServlet.java @@ -11,6 +11,8 @@ package org.cryptomator.frontend.webdav; import java.io.IOException; import java.util.EnumSet; +import javax.inject.Inject; +import javax.inject.Singleton; import javax.servlet.DispatcherType; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -21,12 +23,24 @@ import org.cryptomator.frontend.webdav.filters.LoopbackFilter; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; -/** - * The server needs to respond to requests to the root resource, because Windows is stupid. - */ -public class WindowsCompatibilityServlet extends HttpServlet { +@Singleton +class DefaultServlet extends HttpServlet { private static final String ROOT_PATH = "/"; + private static final String WILDCARD = "/*"; + + private final Tarpit tarpit; + + @Inject + public DefaultServlet(Tarpit tarpit) { + this.tarpit = tarpit; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + tarpit.handle(req); + super.service(req, resp); + } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -36,11 +50,11 @@ public class WindowsCompatibilityServlet extends HttpServlet { resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } - public static ServletContextHandler createServletContextHandler() { + public ServletContextHandler createServletContextHandler() { final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS); - final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, WindowsCompatibilityServlet.class); + final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, this); servletContext.addServlet(servletHolder, ROOT_PATH); - servletContext.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST)); + servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); return servletContext; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java new file mode 100644 index 000000000..011adddc6 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2016 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + *******************************************************************************/ +package org.cryptomator.frontend.webdav; + +import static java.lang.Math.max; +import static java.lang.System.currentTimeMillis; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServletRequest; + +import org.cryptomator.frontend.FrontendId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +class Tarpit implements Serializable { + + private static final Logger LOG = LoggerFactory.getLogger(Tarpit.class); + private static final long DELAY_MS = 10000; + + private final Set validFrontendIds = new HashSet<>(); + + @Inject + public Tarpit() { + } + + public void register(FrontendId frontendId) { + validFrontendIds.add(frontendId); + } + + public void unregister(FrontendId frontendId) { + validFrontendIds.remove(frontendId); + } + + public void handle(HttpServletRequest req) { + if (isRequestWithInvalidVaultId(req)) { + delayExecutionUninterruptibly(); + LOG.debug("Delayed request to " + req.getRequestURI() + " by " + DELAY_MS + "ms"); + } + } + + private boolean isRequestWithInvalidVaultId(HttpServletRequest req) { + Optional frontendId = ContextPaths.extractFrontendId(req.getServletPath()); + return frontendId.isPresent() && !isValid(frontendId.get()); + } + + private void delayExecutionUninterruptibly() { + long expected = currentTimeMillis() + DELAY_MS; + long sleepTime = DELAY_MS; + while (expected > currentTimeMillis()) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + sleepTime = max(0, currentTimeMillis() - expected + 10); + } + } + } + + private boolean isValid(FrontendId frontendId) { + return validFrontendIds.contains(frontendId); + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java index 87ca8abee..f80e5543b 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java @@ -24,12 +24,15 @@ class WebDavFrontend implements Frontend { private final WebDavMounterProvider webdavMounterProvider; private final ServletContextHandler handler; private final URI uri; + private final Runnable afterClose; + private WebDavMount mount; - public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException { + public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri, Runnable afterUnmount) throws FrontendCreationFailedException { this.webdavMounterProvider = webdavMounterProvider; this.handler = handler; this.uri = uri; + this.afterClose = afterUnmount; try { handler.start(); } catch (Exception e) { @@ -39,19 +42,23 @@ class WebDavFrontend implements Frontend { @Override public void close() throws Exception { - unmount(); - handler.stop(); + try { + unmount(); + handler.stop(); + } finally { + afterClose.run(); + } } @Override public void mount(Map> mountParams) throws CommandFailedException { - mount = webdavMounterProvider.get().mount(uri, mountParams); + mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams); } - @Override - public void unmount() throws CommandFailedException { + private void unmount() throws CommandFailedException { if (mount != null) { mount.unmount(); + mount = null; } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java new file mode 100644 index 000000000..7603d867e --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java @@ -0,0 +1,11 @@ +package org.cryptomator.frontend.webdav; + +import org.cryptomator.common.CommonsModule; +import org.cryptomator.frontend.webdav.mount.WebDavMounterModule; + +import dagger.Module; + +@Module(includes = {CommonsModule.class, WebDavMounterModule.class}) +public class WebDavModule { + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java index 1b9076ea3..0f18bbab5 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.frontend.webdav; +import static java.lang.String.format; + import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.BlockingQueue; @@ -20,6 +22,7 @@ import org.cryptomator.filesystem.Folder; import org.cryptomator.frontend.Frontend; import org.cryptomator.frontend.FrontendCreationFailedException; import org.cryptomator.frontend.FrontendFactory; +import org.cryptomator.frontend.FrontendId; import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; @@ -45,9 +48,10 @@ public class WebDavServer implements FrontendFactory { private final ContextHandlerCollection servletCollection; private final WebDavServletContextFactory servletContextFactory; private final WebDavMounterProvider webdavMounterProvider; + private final Tarpit tarpit; @Inject - WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider) { + WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider, DefaultServlet defaultServlet, Tarpit tarpit) { final BlockingQueue queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS); final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue); this.server = new Server(tp); @@ -55,8 +59,9 @@ public class WebDavServer implements FrontendFactory { this.servletCollection = new ContextHandlerCollection(); this.servletContextFactory = servletContextFactory; this.webdavMounterProvider = webdavMounterProvider; - - servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler()); + this.tarpit = tarpit; + + servletCollection.addHandler(defaultServlet.createServletContextHandler()); server.setConnectors(new Connector[] {localConnector}); server.setHandler(servletCollection); } @@ -103,10 +108,8 @@ public class WebDavServer implements FrontendFactory { } @Override - public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException { - if (!contextPath.startsWith("/")) { - throw new IllegalArgumentException("contextPath must begin with '/'"); - } + public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException { + String contextPath = format("/%s/%s", id, name); final URI uri; try { uri = new URI("http", null, "localhost", getPort(), contextPath, null, null); @@ -114,8 +117,9 @@ public class WebDavServer implements FrontendFactory { throw new IllegalStateException(e); } final ServletContextHandler handler = addServlet(root, uri); + tarpit.register(id); LOG.info("Servlet available under " + uri); - return new WebDavFrontend(webdavMounterProvider, handler, uri); + return new WebDavFrontend(webdavMounterProvider, handler, uri, () -> tarpit.unregister(id)); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java index 3fc0454c8..85915d192 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java @@ -22,6 +22,7 @@ import org.cryptomator.frontend.webdav.filters.AcceptRangeFilter; import org.cryptomator.frontend.webdav.filters.LoopbackFilter; import org.cryptomator.frontend.webdav.filters.MacChunkedPutCompatibilityFilter; import org.cryptomator.frontend.webdav.filters.MkcolComplianceFilter; +import org.cryptomator.frontend.webdav.filters.PostRequestBlockingFilter; import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter; import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker; import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType; @@ -34,10 +35,9 @@ import org.eclipse.jetty.servlet.ServletHolder; class WebDavServletContextFactory { private static final String WILDCARD = "/*"; - + @Inject - public WebDavServletContextFactory() { - } + public WebDavServletContextFactory() {} /** * Creates a new Jetty ServletContextHandler, that can be be added to a servletCollection as follows: @@ -67,6 +67,7 @@ class WebDavServletContextFactory { final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root)); servletContext.addServlet(servletHolder, WILDCARD); servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); + servletContext.addFilter(PostRequestBlockingFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST)); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostFromAllowHeaderRemovingHttpServletResponseWrapper.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostFromAllowHeaderRemovingHttpServletResponseWrapper.java new file mode 100644 index 000000000..8e2751a1b --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostFromAllowHeaderRemovingHttpServletResponseWrapper.java @@ -0,0 +1,43 @@ +package org.cryptomator.frontend.webdav.filters; + +import static java.util.Arrays.stream; +import static java.util.function.Predicate.isEqual; +import static java.util.stream.Collectors.joining; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +class PostFromAllowHeaderRemovingHttpServletResponseWrapper extends HttpServletResponseWrapper { + + public PostFromAllowHeaderRemovingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public void addHeader(String name, String value) { + if (isAllowHeader(name)) { + super.setHeader(name, removePost(value)); + } else { + super.addHeader(name, value); + } + } + + @Override + public void setHeader(String name, String value) { + if (isAllowHeader(name)) { + super.setHeader(name, removePost(value)); + } else { + super.setHeader(name, value); + } + } + + private String removePost(String value) { + return stream(value.split("\\s*,\\s*")) + .filter(isEqual("POST").negate()) + .collect(joining(", ")); + } + + private boolean isAllowHeader(String name) { + return "allow".equalsIgnoreCase(name); + } +} \ No newline at end of file diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostRequestBlockingFilter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostRequestBlockingFilter.java new file mode 100644 index 000000000..1c948e5db --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/PostRequestBlockingFilter.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.frontend.webdav.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Blocks all post requests. + */ +public class PostRequestBlockingFilter implements HttpFilter { + + private static final String POST_METHOD = "POST"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no-op + } + + @Override + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (isPost(request)) { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } else { + chain.doFilter(request, new PostFromAllowHeaderRemovingHttpServletResponseWrapper(response)); + } + } + + private boolean isPost(HttpServletRequest request) { + return POST_METHOD.equalsIgnoreCase(request.getMethod()); + } + + @Override + public void destroy() { + // no-op + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java index 5e471a00d..bdde344b6 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java @@ -40,6 +40,11 @@ import com.google.common.io.ByteStreams; class DavFile extends DavNode { private static final Logger LOG = LoggerFactory.getLogger(DavFile.class); + protected static final String CONTENT_TYPE_VALUE = "application/octet-stream"; + protected static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition"; + protected static final String CONTENT_DISPOSITION_VALUE = "attachment"; + protected static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options"; + protected static final String X_CONTENT_TYPE_OPTIONS_VALUE = "nosniff"; public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, FileLocator node) { super(factory, lockManager, session, node); @@ -56,6 +61,9 @@ class DavFile extends DavNode { if (!outputContext.hasStream()) { return; } + outputContext.setContentType(CONTENT_TYPE_VALUE); + outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE); + outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE); try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) { outputContext.setContentLength(src.size()); ByteStreams.copy(src, dst); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java index 6382b8017..96a0bb547 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java @@ -57,6 +57,9 @@ class DavFileWithRange extends DavFile { final Long rangeLength = range.getRight() - range.getLeft() + 1; outputContext.setContentLength(rangeLength); outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), contentRangeResponseHeader(range.getLeft(), range.getRight(), contentLength)); + outputContext.setContentType(CONTENT_TYPE_VALUE); + outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE); + outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE); src.position(range.getLeft()); InputStream limitedIn = ByteStreams.limit(Channels.newInputStream(src), rangeLength); ByteStreams.copy(limitedIn, out); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/ExclusiveSharedLockManager.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/ExclusiveSharedLockManager.java index b94ee9ed8..88225c855 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/ExclusiveSharedLockManager.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/ExclusiveSharedLockManager.java @@ -69,7 +69,8 @@ class ExclusiveSharedLockManager implements LockManager { } String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID(); - return lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()).computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo)); + Map lockMap = Objects.requireNonNull(lockedResources.computeIfAbsent(locator, loc -> new HashMap<>())); + return lockMap.computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo)); } private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) { diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java index 7d83f10af..a05d19bd1 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java @@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam; final class FallbackWebDavMounter implements WebDavMounterStrategy { @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return true; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java new file mode 100644 index 000000000..51834277c --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + * Markus Kreusch - Refactored WebDavMounter to use strategy pattern + * Mohit Raju - Added fallback schema-name "webdav" when opening file managers + ******************************************************************************/ +package org.cryptomator.frontend.webdav.mount; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.frontend.CommandFailedException; +import org.cryptomator.frontend.Frontend.MountParam; +import org.cryptomator.frontend.webdav.mount.command.Script; + +@Singleton +final class LinuxGvfsDavMounter implements WebDavMounterStrategy { + + @Inject + LinuxGvfsDavMounter() { + } + + @Override + public boolean shouldWork(Map> mountParams) { + if (SystemUtils.IS_OS_LINUX) { + Optional prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty()); + boolean prefSchemeIsUnspecifiedOrDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("dav"); + final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open"); + try { + checkScripts.execute(); + return prefSchemeIsUnspecifiedOrDav; + } catch (CommandFailedException e) { + return false; + } + } else { + return false; + } + } + + @Override + public void warmUp(int serverPort) { + // no-op + } + + @Override + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final Script mountScript = Script.fromLines("set -x", "gvfs-mount \"dav:$DAV_SSP\"").addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); + mountScript.execute(); + return new LinuxGvfsDavMount(uri); + } + + private static class LinuxGvfsDavMount extends AbstractWebDavMount { + private final URI webDavUri; + private final Script testMountStillExistsScript; + private final Script unmountScript; + + private LinuxGvfsDavMount(URI webDavUri) { + this.webDavUri = webDavUri; + this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + } + + @Override + public void unmount() throws CommandFailedException { + boolean mountStillExists; + try { + testMountStillExistsScript.execute(); + mountStillExists = true; + } catch (CommandFailedException e) { + mountStillExists = false; + } + // only attempt unmount if user didn't unmount manually: + if (mountStillExists) { + unmountScript.execute(); + } + } + + @Override + public void reveal() throws CommandFailedException { + Script.fromLines("set -x", "gvfs-open \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute(); + } + + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java index 1feab0d2d..219709294 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java @@ -30,12 +30,14 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { if (SystemUtils.IS_OS_LINUX) { + Optional prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty()); + boolean prefSchemeIsUnspecifiedOrWebDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("webdav"); final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open"); try { checkScripts.execute(); - return true; + return prefSchemeIsUnspecifiedOrWebDav; } catch (CommandFailedException e) { return false; } @@ -84,15 +86,7 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { @Override public void reveal() throws CommandFailedException { - try { - openMountWithWebdavUri("dav:" + webDavUri.getRawSchemeSpecificPart()).execute(); - } catch (CommandFailedException exception) { - openMountWithWebdavUri("webdav:" + webDavUri.getRawSchemeSpecificPart()).execute(); - } - } - - private Script openMountWithWebdavUri(String webdavUri) { - return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri); + Script.fromLines("set -x", "gvfs-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute(); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java index f381e2e6f..0bd477541 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java @@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java index 89e8a86a2..8e834821a 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java @@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java deleted file mode 100644 index 3cd1b2a08..000000000 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.frontend.webdav.mount; - -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; - -import java.util.Collection; -import java.util.Iterator; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -class MountStrategies implements Collection { - - private final Collection delegate; - - @Inject - MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { - delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter)); - } - - @Override - public int size() { - return delegate.size(); - } - - @Override - public boolean isEmpty() { - return delegate.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return delegate.contains(o); - } - - @Override - public Iterator iterator() { - return delegate.iterator(); - } - - @Override - public Object[] toArray() { - return delegate.toArray(); - } - - @Override - public T[] toArray(T[] a) { - return delegate.toArray(a); - } - - @Override - public boolean add(WebDavMounterStrategy e) { - return delegate.add(e); - } - - @Override - public boolean remove(Object o) { - return delegate.remove(o); - } - - @Override - public boolean containsAll(Collection c) { - return delegate.containsAll(c); - } - - @Override - public boolean addAll(Collection c) { - return delegate.addAll(c); - } - - @Override - public boolean removeAll(Collection c) { - return delegate.removeAll(c); - } - - @Override - public boolean retainAll(Collection c) { - return delegate.retainAll(c); - } - - @Override - public void clear() { - delegate.clear(); - } - - @Override - public boolean equals(Object o) { - return delegate.equals(o); - } - - @Override - public int hashCode() { - return delegate.hashCode(); - } - -} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java new file mode 100644 index 000000000..c42151f40 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java @@ -0,0 +1,31 @@ +package org.cryptomator.frontend.webdav.mount; + +import java.util.Set; + +import javax.inject.Named; +import javax.inject.Singleton; + +import com.google.common.collect.Sets; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +@Module +public class WebDavMounterModule { + + @Provides + @ElementsIntoSet + static Set provideMounters(LinuxGvfsWebDavMounter linuxWebDavMounter, LinuxGvfsDavMounter linuxDavMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, + MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { + return Sets.newHashSet(linuxWebDavMounter, linuxDavMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter); + } + + @Provides + @Singleton + @Named("fallback") + static WebDavMounterStrategy provideFallbackStrategy() { + return new FallbackWebDavMounter(); + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java index ae5341f9d..a1bdf1745 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java @@ -10,34 +10,35 @@ package org.cryptomator.frontend.webdav.mount; import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import javax.inject.Inject; -import javax.inject.Provider; +import javax.inject.Named; import javax.inject.Singleton; +import org.cryptomator.frontend.Frontend.MountParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton -public class WebDavMounterProvider implements Provider { +public class WebDavMounterProvider { private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class); - private final WebDavMounterStrategy choosenStrategy; + private final Collection availableStrategies; + private final WebDavMounterStrategy fallbackStrategy; @Inject - public WebDavMounterProvider(MountStrategies availableStrategies) { - this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies); + public WebDavMounterProvider(Set availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) { + this.availableStrategies = availableStrategies; + this.fallbackStrategy = fallbackStrategy; } - @Override - public WebDavMounter get() { - return this.choosenStrategy; - } - - private WebDavMounterStrategy getStrategyWhichShouldWork(Collection availableStrategies) { - WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter()); - LOG.info("Using {}", strategy.getClass().getSimpleName()); - return strategy; + public WebDavMounter chooseMounter(Map> mountParams) { + WebDavMounterStrategy result = availableStrategies.stream().filter(strategy -> strategy.shouldWork(mountParams)).findFirst().orElse(fallbackStrategy); + LOG.info("Using {}", result.getClass().getSimpleName()); + return result; } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java index 99ec00162..ae5d6b687 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java @@ -9,6 +9,11 @@ ******************************************************************************/ package org.cryptomator.frontend.webdav.mount; +import java.util.Map; +import java.util.Optional; + +import org.cryptomator.frontend.Frontend.MountParam; + /** * A strategy able to mount a webdav share and display it to the user. * @@ -19,7 +24,7 @@ interface WebDavMounterStrategy extends WebDavMounter { /** * @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work */ - boolean shouldWork(); + boolean shouldWork(Map> mountParams); /** * Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster, diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsDriveLetters.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsDriveLetters.java index 10692e0da..76ed8d71d 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsDriveLetters.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsDriveLetters.java @@ -9,11 +9,11 @@ package org.cryptomator.frontend.webdav.mount; import static java.util.stream.Collectors.toSet; -import static java.util.stream.IntStream.rangeClosed; import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.Set; +import java.util.stream.IntStream; import java.util.stream.StreamSupport; import javax.inject.Inject; @@ -24,16 +24,21 @@ import org.apache.commons.lang3.SystemUtils; import com.google.common.collect.Sets; - @Singleton public final class WindowsDriveLetters { - - private static final Set A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet()); - + + private static final Set A_TO_Z; + + static { + try (IntStream stream = IntStream.rangeClosed('A', 'Z')) { + A_TO_Z = stream.mapToObj(i -> (char) i).collect(toSet()); + } + } + @Inject public WindowsDriveLetters() { } - + public Set getOccupiedDriveLetters() { if (!SystemUtils.IS_OS_WINDOWS) { throw new UnsupportedOperationException("This method is only defined for Windows file systems"); @@ -41,7 +46,7 @@ public final class WindowsDriveLetters { Iterable rootDirs = FileSystems.getDefault().getRootDirectories(); return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet()); } - + public Set getAvailableDriveLetters() { return Sets.difference(A_TO_Z, getOccupiedDriveLetters()); } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java index ee8fd5b96..bea6a5cdd 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java @@ -61,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_WINDOWS; } @@ -102,7 +102,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } - + private void addProxyOverrides(URI uri) throws IOException, CommandFailedException { try { // get existing value for ProxyOverride key from reqistry: @@ -110,7 +110,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { Process queryCmd = query.start(); String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8); int queryResult = queryCmd.waitFor(); - + // determine new value for ProxyOverride key: Set overrides = new HashSet<>(); Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut); @@ -122,7 +122,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { overrides.add(""); overrides.add(uri.getHost()); overrides.add(uri.getHost() + ":" + uri.getPort()); - + // set new value: String overridesStr = StringUtils.join(overrides, ';'); ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f"); @@ -133,6 +133,8 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8); throw new CommandFailedException(addStdErr); } + } catch (IOException | CommandFailedException e) { + LOG.info("Failed to add proxy overrides", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); InterruptedIOException ioException = new InterruptedIOException(); @@ -158,7 +160,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { private WindowsWebDavMount(String driveLetter) { this.driveLetter = CharUtils.toCharacterObject(driveLetter); this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":"); - this.unmountScript = fromLines("net use " + driveLetter + ": /delete"); + this.unmountScript = fromLines("net use " + driveLetter + ": /delete /no"); } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/command/CommandRunner.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/command/CommandRunner.java index 0184358c6..8df93e237 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/command/CommandRunner.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/command/CommandRunner.java @@ -61,13 +61,18 @@ final class CommandRunner { static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException { try { final List env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList()); + final String[] lines = script.getLines(); + if (lines.length == 0) { + throw new IllegalArgumentException("Invalid script"); + } CommandResult result = null; - for (final String line : script.getLines()) { + for (final String line : lines) { final String[] cmds = ArrayUtils.add(determineCli(), line); final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0])); result = run(proc, timeout, unit); result.assertOk(); } + assert result != null; return result; } catch (IOException e) { throw new CommandFailedException(e); diff --git a/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/DefaultServletTest.java similarity index 78% rename from main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java rename to main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/DefaultServletTest.java index 4a5feaf9d..5d87a9b2b 100644 --- a/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java +++ b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/DefaultServletTest.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.frontend.webdav; +import static org.mockito.Mockito.mock; + import java.io.IOException; import javax.servlet.Servlet; @@ -21,21 +23,26 @@ import org.junit.Test; import org.mockito.Mockito; -public class WindowsCompatibilityServletTest { +public class DefaultServletTest { + + private Tarpit tarpit = mock(Tarpit.class); + + private DefaultServlet inTest = new DefaultServlet(tarpit); @Test public void testFactory() throws ServletException { - ServletHolder[] holders = WindowsCompatibilityServlet.createServletContextHandler().getServletHandler().getServlets(); + + ServletHolder[] holders = inTest.createServletContextHandler().getServletHandler().getServlets(); Assert.assertEquals(1, holders.length); ServletHolder holder = holders[0]; Servlet servlet = holder.getServlet(); - Assert.assertTrue(servlet instanceof WindowsCompatibilityServlet); + Assert.assertTrue(servlet instanceof DefaultServlet); } @Test public void testResponse() throws IOException, ServletException { - final WindowsCompatibilityServlet servlet = new WindowsCompatibilityServlet(); + final DefaultServlet servlet = inTest; final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java similarity index 86% rename from main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java rename to main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java index 68f1a43e7..cbeae7914 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java +++ b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java @@ -10,12 +10,10 @@ package org.cryptomator.frontend.webdav; import javax.inject.Singleton; -import org.cryptomator.common.CommonsModule; - import dagger.Component; @Singleton -@Component(modules = {CommonsModule.class}) +@Component(modules = {WebDavModule.class}) public interface WebDavComponent { WebDavServer server(); diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml index 9416d0a41..cd76ed0c3 100644 --- a/main/jacoco-report/pom.xml +++ b/main/jacoco-report/pom.xml @@ -5,7 +5,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 jacoco-report Cryptomator Code Coverage Report diff --git a/main/pom.xml b/main/pom.xml index 4bbec2aad..718936741 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -7,7 +7,7 @@ 4.0.0 org.cryptomator main - 1.1.3 + 1.1.4 pom Cryptomator diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index e711e3632..43c6bb55a 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 uber-jar pom diff --git a/main/ui/pom.xml b/main/ui/pom.xml index da568ec75..c1f1f07f8 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.3 + 1.1.4 ui Cryptomator GUI diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java index b32935193..095caeb89 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -14,6 +14,7 @@ import javax.inject.Singleton; import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DeferredCloser; import dagger.Component; @@ -21,6 +22,9 @@ import dagger.Component; @Singleton @Component(modules = CryptomatorModule.class) interface CryptomatorComponent { + + AsyncTaskService asyncTaskService(); + ExecutorService executorService(); DeferredCloser deferredCloser(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index 3740b566d..b62f631c4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -17,13 +17,14 @@ import javax.inject.Singleton; import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; +import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; -import org.cryptomator.frontend.webdav.mount.WebDavMounter; -import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; import org.cryptomator.ui.util.DeferredCloser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; @@ -32,9 +33,10 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) class CryptomatorModule { + private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); private final Application application; private final Stage mainWindow; @@ -60,7 +62,13 @@ class CryptomatorModule { @Singleton DeferredCloser provideDeferredCloser() { DeferredCloser closer = new DeferredCloser(); - Cryptomator.addShutdownTask(closer::close); + Cryptomator.addShutdownTask(() -> { + try { + closer.close(); + } catch (Exception e) { + LOG.error("Error during shutdown.", e); + } + }); return closer; } @@ -83,12 +91,6 @@ class CryptomatorModule { return closer.closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown).get().orElseThrow(IllegalStateException::new); } - @Provides - @Singleton - WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) { - return webDavMounterProvider.get(); - } - @Provides @Singleton FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Settings settings) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 86c16261b..d28721ffa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.controllers.MainController; @@ -126,7 +127,11 @@ public class MainApplication extends Application { @Override public void stop() { - closer.close(); + try { + closer.close(); + } catch (ExecutionException e) { + LOG.error("Error closing ressources", e); + } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 7c1532775..34ef59b0d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -22,6 +22,7 @@ import org.fxmisc.easybind.EasyBind; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; @@ -52,6 +53,12 @@ public class SettingsController extends LocalizedFXMLViewController { @FXML private Label versionLabel; + @FXML + private Label prefGvfsSchemeLabel; + + @FXML + private ChoiceBox prefGvfsScheme; + @Override public void initialize() { checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally()); @@ -62,10 +69,16 @@ public class SettingsController extends LocalizedFXMLViewController { useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS); useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); + prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX); + prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX); + prefGvfsScheme.getItems().add("dav"); + prefGvfsScheme.getItems().add("webdav"); + prefGvfsScheme.setValue(settings.getPreferredGvfsScheme()); EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange); EasyBind.subscribe(portField.textProperty(), this::portDidChange); EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange); + EasyBind.subscribe(prefGvfsScheme.valueProperty(), this::prefGvfsSchemeDidChange); } @Override @@ -101,6 +114,11 @@ public class SettingsController extends LocalizedFXMLViewController { settings.save(); } + private void prefGvfsSchemeDidChange(String newValue) { + settings.setPreferredGvfsScheme(newValue); + settings.save(); + } + private void filterNumericKeyEvents(KeyEvent t) { if (t.getCharacter() == null || t.getCharacter().length() == 0) { return; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index ad8cd5cac..d17175dc7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -11,7 +11,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Comparator; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; @@ -27,6 +26,7 @@ import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +56,7 @@ public class UnlockController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); private final Application app; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private final Lazy frontendFactory; private final Settings settings; private final WindowsDriveLetters driveLetters; @@ -65,10 +65,10 @@ public class UnlockController extends LocalizedFXMLViewController { private Optional listener = Optional.empty(); @Inject - public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { + public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { super(localization); this.app = app; - this.exec = exec; + this.asyncTaskService = asyncTaskService; this.frontendFactory = frontendFactory; this.settings = settings; this.driveLetters = driveLetters; @@ -246,6 +246,7 @@ public class UnlockController extends LocalizedFXMLViewController { return; } vault.get().setWinDriveLetter(newValue); + settings.save(); } private void chooseSelectedDriveLetter() { @@ -274,8 +275,7 @@ public class UnlockController extends LocalizedFXMLViewController { progressIndicator.setVisible(true); downloadsPageLink.setVisible(false); CharSequence password = passwordField.getCharacters(); - exec.submit(() -> this.unlock(vault.get(), password)); - + asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run(); } private void unlock(Vault vault, CharSequence password) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 9cc58ecf1..3d0e58aa4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -10,7 +10,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Provider; @@ -19,6 +18,7 @@ import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.ActiveWindowStyleSupport; +import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; import javafx.animation.Animation; @@ -52,16 +52,16 @@ public class UnlockedController extends LocalizedFXMLViewController { private final Stage macWarningsWindow = new Stage(); private final MacWarningsController macWarningsController; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); private Timeline ioAnimation; @Inject - public UnlockedController(Localization localization, Provider macWarningsControllerProvider, ExecutorService exec) { + public UnlockedController(Localization localization, Provider macWarningsControllerProvider, AsyncTaskService asyncTaskService) { super(localization); this.macWarningsController = macWarningsControllerProvider.get(); - this.exec = exec; + this.asyncTaskService = asyncTaskService; macWarningsController.vault.bind(this.vault); } @@ -116,18 +116,13 @@ public class UnlockedController extends LocalizedFXMLViewController { @FXML private void didClickLockVault(ActionEvent event) { - exec.submit(() -> { - try { - vault.get().unmount(); - } catch (CommandFailedException e) { - Platform.runLater(() -> { - messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); - }); - return; - } + asyncTaskService.asyncTaskOf(() -> { vault.get().deactivateFrontend(); - listener.ifPresent(this::invokeListenerLater); - }); + }).onSuccess(() -> { + listener.ifPresent(listener -> listener.didLock(this)); + }).onError(Exception.class, () -> { + messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + }).run(); } @FXML @@ -142,15 +137,11 @@ public class UnlockedController extends LocalizedFXMLViewController { @FXML private void didClickRevealVault(ActionEvent event) { - exec.submit(() -> { - try { - vault.get().reveal(); - } catch (CommandFailedException e) { - Platform.runLater(() -> { - messageLabel.setText(localization.getString("unlocked.label.revealFailed")); - }); - } - }); + asyncTaskService.asyncTaskOf(() -> { + vault.get().reveal(); + }).onError(CommandFailedException.class, () -> { + messageLabel.setText(localization.getString("unlocked.label.revealFailed")); + }).run(); } @FXML @@ -258,12 +249,6 @@ public class UnlockedController extends LocalizedFXMLViewController { this.listener = Optional.ofNullable(listener); } - private void invokeListenerLater(LockListener listener) { - Platform.runLater(() -> { - listener.didLock(this); - }); - } - @FunctionalInterface interface LockListener { void didLock(UnlockedController ctrl); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java index db92d7df4..729f3fdf9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java @@ -3,7 +3,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; @@ -13,11 +12,9 @@ import org.cryptomator.ui.model.UpgradeStrategy; import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; @@ -28,19 +25,17 @@ import javafx.scene.control.ProgressIndicator; public class UpgradeController extends LocalizedFXMLViewController { - private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class); - final ObjectProperty vault = new SimpleObjectProperty<>(); final ObjectProperty> strategy = new SimpleObjectProperty<>(); private final UpgradeStrategies strategies; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private Optional listener = Optional.empty(); @Inject - public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) { + public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) { super(localization); this.strategies = strategies; - this.exec = exec; + this.asyncTaskService = asyncTaskService; } @FXML @@ -103,26 +98,22 @@ public class UpgradeController extends LocalizedFXMLViewController { Vault v = Objects.requireNonNull(vault.getValue()); passwordField.setDisable(true); progressIndicator.setVisible(true); - exec.submit(() -> { - if (!instruction.isApplicable(v)) { - LOG.error("No upgrade needed for " + v.path().getValue()); - throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); - } - try { - instruction.upgrade(v, passwordField.getCharacters()); - Platform.runLater(this::showNextUpgrade); - } catch (UpgradeFailedException e) { - Platform.runLater(() -> { + asyncTaskService // + .asyncTaskOf(() -> { + if (!instruction.isApplicable(v)) { + throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); + } + instruction.upgrade(v, passwordField.getCharacters()); + }) // + .onSuccess(this::showNextUpgrade) // + .onError(UpgradeFailedException.class, e -> { errorLabel.setText(e.getLocalizedMessage()); - }); - } finally { - Platform.runLater(() -> { + }) // + .andFinally(() -> { progressIndicator.setVisible(false); passwordField.setDisable(false); passwordField.swipe(); - }); - } - }); + }).run(); } private void showNextUpgrade() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index b7c41776a..143a43ef9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -8,14 +8,12 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Named; @@ -31,6 +29,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.AsyncTaskService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,15 +53,15 @@ public class WelcomeController extends LocalizedFXMLViewController { private final Application app; private final Settings settings; private final Comparator semVerComparator; - private final ExecutorService executor; + private final AsyncTaskService asyncTaskService; @Inject - public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, ExecutorService executor) { + public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, AsyncTaskService asyncTaskService) { super(localization); this.app = app; this.settings = settings; this.semVerComparator = semVerComparator; - this.executor = executor; + this.asyncTaskService = asyncTaskService; } @FXML @@ -82,7 +81,7 @@ public class WelcomeController extends LocalizedFXMLViewController { if (areUpdatesManagedExternally()) { checkForUpdatesContainer.setVisible(false); } else if (settings.isCheckForUpdatesEnabled()) { - executor.execute(this::checkForUpdates); + this.checkForUpdates(); } } @@ -100,16 +99,14 @@ public class WelcomeController extends LocalizedFXMLViewController { } private void checkForUpdates() { - Platform.runLater(() -> { - checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking")); - checkForUpdatesIndicator.setVisible(true); - }); - final HttpClient client = new HttpClient(); - final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json"); - client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT")); - client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); - client.getParams().setConnectionManagerTimeout(5000); - try { + checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking")); + checkForUpdatesIndicator.setVisible(true); + asyncTaskService.asyncTaskOf(() -> { + final HttpClient client = new HttpClient(); + final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json"); + client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT")); + client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); + client.getParams().setConnectionManagerTimeout(5000); client.executeMethod(method); final InputStream responseBodyStream = method.getResponseBodyAsStream(); if (method.getStatusCode() == HttpStatus.SC_OK && responseBodyStream != null) { @@ -121,14 +118,10 @@ public class WelcomeController extends LocalizedFXMLViewController { this.compareVersions(map); } } - } catch (IOException e) { - // no error handling required. Maybe next time the version check is successful. - } finally { - Platform.runLater(() -> { - checkForUpdatesStatus.setText(""); - checkForUpdatesIndicator.setVisible(false); - }); - } + }).andFinally(() -> { + checkForUpdatesStatus.setText(""); + checkForUpdatesIndicator.setVisible(false); + }).run(); } private Optional applicationVersion() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index d38f9152c..07c83530d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.ui.model; +import static org.apache.commons.lang3.StringUtils.stripStart; + import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.FileAlreadyExistsException; @@ -40,6 +42,7 @@ import org.cryptomator.frontend.Frontend; import org.cryptomator.frontend.Frontend.MountParam; import org.cryptomator.frontend.FrontendCreationFailedException; import org.cryptomator.frontend.FrontendFactory; +import org.cryptomator.frontend.FrontendId; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.util.DeferredClosable; import org.cryptomator.ui.util.DeferredCloser; @@ -70,6 +73,7 @@ public class Vault implements CryptoFileSystemDelegate { private final ObservableList namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList()); private final Set whitelistedResourcesWithInvalidMac = new HashSet<>(); private final AtomicReference nioFileSystem = new AtomicReference<>(); + private final String id; private String mountName; private Character winDriveLetter; @@ -78,13 +82,15 @@ public class Vault implements CryptoFileSystemDelegate { /** * Package private constructor, use {@link VaultFactory}. + * + * @param string */ - Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) { + Vault(String id, Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) { this.path = new SimpleObjectProperty(vaultDirectoryPath); this.shorteningFileSystemFactory = shorteningFileSystemFactory; this.cryptoFileSystemFactory = cryptoFileSystemFactory; this.closer = closer; - + this.id = id; try { setMountName(name().getValue()); } catch (IllegalArgumentException e) { @@ -129,8 +135,7 @@ public class Vault implements CryptoFileSystemDelegate { FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC); StatsFileSystem statsFs = new StatsFileSystem(normalizingFs); statsFileSystem = Optional.of(statsFs); - String contextPath = StringUtils.prependIfMissing(mountName, "/"); - Frontend frontend = frontendFactory.create(statsFs, contextPath); + Frontend frontend = frontendFactory.create(statsFs, FrontendId.from(id), stripStart(mountName, "/")); filesystemFrontend = closer.closeLater(frontend); frontend.mount(getMountParams(settings)); success = true; @@ -143,7 +148,7 @@ public class Vault implements CryptoFileSystemDelegate { } } - public synchronized void deactivateFrontend() { + public synchronized void deactivateFrontend() throws Exception { filesystemFrontend.close(); statsFileSystem = Optional.empty(); Platform.runLater(() -> unlocked.set(false)); @@ -154,7 +159,8 @@ public class Vault implements CryptoFileSystemDelegate { return ImmutableMap.of( // MountParam.MOUNT_NAME, Optional.ofNullable(mountName), // MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)), // - MountParam.HOSTNAME, Optional.of(hostname) // + MountParam.HOSTNAME, Optional.of(hostname), // + MountParam.PREFERRED_GVFS_SCHEME, Optional.ofNullable(settings.getPreferredGvfsScheme()) // ); } @@ -162,10 +168,6 @@ public class Vault implements CryptoFileSystemDelegate { Optionals.ifPresent(filesystemFrontend.get(), Frontend::reveal); } - public void unmount() throws CommandFailedException { - Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount); - } - // ****************************************************************************** // Delegate methods // ********************************************************************************/ @@ -305,6 +307,10 @@ public class Vault implements CryptoFileSystemDelegate { this.winDriveLetter = winDriveLetter; } + public String getId() { + return id; + } + // ****************************************************************************** // Hashcode / Equals // *******************************************************************************/ diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java index ec0b0d420..cf1dcc756 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java @@ -15,6 +15,7 @@ import javax.inject.Singleton; import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory; import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory; +import org.cryptomator.frontend.FrontendId; import org.cryptomator.ui.util.DeferredCloser; @Singleton @@ -31,8 +32,12 @@ public class VaultFactory { this.closer = closer; } + public Vault createVault(String id, Path path) { + return new Vault(id, path, shorteningFileSystemFactory, cryptoFileSystemFactory, closer); + } + public Vault createVault(Path path) { - return new Vault(path, shorteningFileSystemFactory, cryptoFileSystemFactory, closer); + return createVault(FrontendId.generate().toString(), path); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java index d71626cf6..90e86abdc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultObjectMapperProvider.java @@ -57,6 +57,7 @@ public class VaultObjectMapperProvider implements Provider { jgen.writeStartObject(); jgen.writeStringField("path", value.path().getValue().toString()); jgen.writeStringField("mountName", value.getMountName()); + jgen.writeStringField("id", value.getId()); final Character winDriveLetter = value.getWinDriveLetter(); if (winDriveLetter != null) { jgen.writeStringField("winDriveLetter", Character.toString(winDriveLetter)); @@ -76,7 +77,12 @@ public class VaultObjectMapperProvider implements Provider { } final String pathStr = node.get("path").asText(); final Path path = FileSystems.getDefault().getPath(pathStr); - final Vault vault = vaultFactoy.createVault(path); + final Vault vault; + if (node.has("id")) { + vault = vaultFactoy.createVault(node.get("id").asText(), path); + } else { + vault = vaultFactoy.createVault(path); + } if (node.has("mountName")) { vault.setMountName(node.get("mountName").asText()); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 6b00aeb41..f9323bf99 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -18,7 +18,7 @@ import org.cryptomator.ui.model.Vault; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications"}) +@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications", "preferredGvfsScheme"}) public class Settings implements Serializable { private static final long serialVersionUID = 7609959894417878744L; @@ -27,6 +27,7 @@ public class Settings implements Serializable { public static final int DEFAULT_PORT = 42427; public static final boolean DEFAULT_USE_IPV6 = false; public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; + public static final String DEFAULT_GVFS_SCHEME = "dav"; private final Consumer saveCmd; @@ -45,6 +46,9 @@ public class Settings implements Serializable { @JsonProperty("numTrayNotifications") private Integer numTrayNotifications; + @JsonProperty("preferredGvfsScheme") + private String preferredGvfsScheme; + /** * Package-private constructor; use {@link SettingsProvider}. */ @@ -113,4 +117,12 @@ public class Settings implements Serializable { this.numTrayNotifications = numTrayNotifications; } + public String getPreferredGvfsScheme() { + return preferredGvfsScheme == null ? DEFAULT_GVFS_SCHEME : preferredGvfsScheme; + } + + public void setPreferredGvfsScheme(String preferredGvfsScheme) { + this.preferredGvfsScheme = preferredGvfsScheme; + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java new file mode 100644 index 000000000..e61a5b774 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java @@ -0,0 +1,174 @@ +package org.cryptomator.ui.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.common.ConsumerThrowingException; +import org.cryptomator.common.RunnableThrowingException; +import org.cryptomator.common.SupplierThrowingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.application.Platform; + +@Singleton +public class AsyncTaskService { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncTaskService.class); + + private final ExecutorService executor; + + @Inject + public AsyncTaskService(ExecutorService executor) { + this.executor = executor; + } + + public AsyncTaskWithoutSuccessHandler asyncTaskOf(RunnableThrowingException task) { + return new AsyncTaskImpl<>(() -> { + task.run(); + return null; + }); + } + + public AsyncTaskWithoutSuccessHandler asyncTaskOf(SupplierThrowingException task) { + return new AsyncTaskImpl<>(task); + } + + private class AsyncTaskImpl implements AsyncTaskWithoutSuccessHandler { + + private final SupplierThrowingException task; + + private ConsumerThrowingException successHandler = value -> { + }; + private List> errorHandlers = new ArrayList<>(); + private RunnableThrowingException finallyHandler = () -> { + }; + + public AsyncTaskImpl(SupplierThrowingException task) { + this.task = task; + } + + @Override + public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler) { + successHandler = handler; + return this; + } + + @Override + public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler) { + return onSuccess(result -> handler.run()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler) { + errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler)); + return this; + } + + @Override + public AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler) { + return onError(type, error -> handler.run()); + } + + @Override + public AsyncTask andFinally(RunnableThrowingException handler) { + finallyHandler = handler; + return this; + } + + @Override + public void run() { + errorHandlers.add(ErrorHandler.LOGGING_HANDLER); + executor.execute(() -> logExceptions(() -> { + try { + ResultType result = task.get(); + Platform.runLater(() -> { + try { + successHandler.accept(result); + } catch (Throwable e) { + LOG.error("Uncaught exception", e); + } + }); + } catch (Throwable e) { + ErrorHandler errorHandler = errorHandlerFor(e); + Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e))); + } finally { + Platform.runLater(toRunnableLoggingException(finallyHandler)); + } + })); + } + + private ErrorHandler errorHandlerFor(Throwable e) { + return errorHandlers.stream().filter(handler -> handler.handles(e)).findFirst().get(); + } + + } + + private static Runnable toRunnableLoggingException(RunnableThrowingException delegate) { + return () -> logExceptions(delegate); + } + + private static void logExceptions(RunnableThrowingException delegate) { + try { + delegate.run(); + } catch (Throwable e) { + LOG.error("Uncaught exception", e); + } + } + + private static class ErrorHandler implements ConsumerThrowingException { + + public static final ErrorHandler LOGGING_HANDLER = new ErrorHandler(Throwable.class, error -> { + LOG.error("Uncaught exception", error); + }); + + private final Class type; + private final ConsumerThrowingException delegate; + + public ErrorHandler(Class type, ConsumerThrowingException delegate) { + this.type = type; + this.delegate = delegate; + } + + public boolean handles(Throwable error) { + return type.isInstance(error); + } + + @Override + public void accept(ErrorType error) throws Throwable { + delegate.accept(error); + } + + } + + public interface AsyncTaskWithoutSuccessHandler extends AsyncTaskWithoutErrorHandler { + + AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler); + + AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler); + + } + + public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler { + + AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler); + + AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler); + + } + + public interface AsyncTaskWithoutFinallyHandler extends AsyncTask { + + AsyncTask andFinally(RunnableThrowingException handler); + + } + + public interface AsyncTask extends Runnable { + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredClosable.java b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredClosable.java index fadb9f7dc..fa8c415ab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredClosable.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredClosable.java @@ -28,12 +28,6 @@ public interface DeferredClosable extends AutoCloseable { */ public Optional get(); - /** - * Quietly closes the Object. If the object was closed before, nothing - * happens. - */ - public void close(); - /** * @return an empty object. */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java index c738e9f59..f2de9324f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java @@ -13,12 +13,10 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import org.cryptomator.common.ConsumerThrowingException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; @@ -31,7 +29,7 @@ import com.google.common.annotations.VisibleForTesting; * *

* If you have a {@link DeferredCloser} instance present, call - * {@link #closeLater(Object, Closer)} immediately after you have opened the + * {@link #closeLater(Object, ConsumerThrowingException)} immediately after you have opened the * resource and return a resource handle. If {@link #close()} is called, the * resource will be closed. Calling {@link DeferredClosable#close()} on the resource * handle will also close the resource and prevent a second closing by @@ -42,8 +40,6 @@ import com.google.common.annotations.VisibleForTesting; */ public class DeferredCloser implements AutoCloseable { - private static final Logger LOG = LoggerFactory.getLogger(DeferredCloser.class); - @VisibleForTesting final Map> cleanups = new ConcurrentSkipListMap<>(); @@ -51,33 +47,32 @@ public class DeferredCloser implements AutoCloseable { final AtomicLong counter = new AtomicLong(); private class ManagedResource implements DeferredClosable { + private final long number = counter.incrementAndGet(); - - private final AtomicReference object = new AtomicReference<>(); + private final T object; private final ConsumerThrowingException closer; + private boolean closed = false; public ManagedResource(T object, ConsumerThrowingException closer) { super(); - this.object.set(object); - this.closer = closer; + this.object = Objects.requireNonNull(object); + this.closer = Objects.requireNonNull(closer); } @Override - public void close() { - final T oldObject = object.getAndSet(null); - if (oldObject != null) { - cleanups.remove(number); - try { - closer.accept(oldObject); - } catch (Exception e) { - LOG.error("Closing resource failed.", e); - } - } + public synchronized void close() throws Exception { + closer.accept(object); + cleanups.remove(number); + closed = true; } @Override public Optional get() throws IllegalStateException { - return Optional.ofNullable(object.get()); + if (closed) { + return Optional.empty(); + } else { + return Optional.of(object); + } } } @@ -85,11 +80,23 @@ public class DeferredCloser implements AutoCloseable { * Closes all added objects which have not been closed before and releases references. */ @Override - public void close() { + public void close() throws ExecutionException { + ExecutionException exception = null; for (Iterator> iterator = cleanups.values().iterator(); iterator.hasNext();) { final ManagedResource closableProvider = iterator.next(); - closableProvider.close(); - iterator.remove(); + try { + closableProvider.close(); + iterator.remove(); + } catch (Exception e) { + if (exception == null) { + exception = new ExecutionException(e); + } else { + exception.addSuppressed(e); + } + } + } + if (exception != null) { + throw exception; } } diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index cb3c407f0..38f453feb 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -325,6 +325,32 @@ -fx-background-color: COLOR_TEXT; } +/******************************************************************************* + * * + * ChoiceBox * + * * + ******************************************************************************/ + +.choice-box { + -fx-background-color: COLOR_BORDER_DARK, COLOR_BACKGROUND; + -fx-background-insets: 0, 1; + -fx-background-radius: 0, 0; + -fx-padding: 0.1em 0.6em 0.1em 0.6em; + -fx-text-fill: COLOR_TEXT; +} + +.choice-box > .open-button > .arrow { + -fx-background-color: transparent, COLOR_TEXT; + -fx-background-insets: 0 0 -1 0, 0; + -fx-padding: 0.166667em 0.333333em 0.166667em 0.333333em; /* 2 4 2 4 */ + -fx-shape: "M 0 0 h 7 l -3.5 4 z"; +} + +.choice-box .context-menu { + -fx-background-color: COLOR_BORDER, #FFF; + -fx-background-insets: 0, 1; +} + /**************************************************************************** * * * ProgressIndicator * diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index 73f26630f..41f770be2 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -15,6 +15,7 @@ +