mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-15 17:21:27 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
560171832c | ||
|
|
6e93d40e51 | ||
|
|
a18c406cf0 | ||
|
|
6730a83cac | ||
|
|
3b3ebd2196 | ||
|
|
505b6542c7 | ||
|
|
31368f0cba | ||
|
|
5b5dd756b1 | ||
|
|
f6ebbb23d1 | ||
|
|
3f0373b08f | ||
|
|
4c3c60060d | ||
|
|
28f275c22d | ||
|
|
24df3c3809 | ||
|
|
034a667e07 | ||
|
|
008e3e3b05 | ||
|
|
94a5bf7596 | ||
|
|
e8db836eff | ||
|
|
429b26f3d8 | ||
|
|
3ae8327300 | ||
|
|
df7e9a0af1 | ||
|
|
93d3eca0ab | ||
|
|
7753d1f0e7 | ||
|
|
d7c6c24932 | ||
|
|
1a75f23081 | ||
|
|
f071efe1b9 | ||
|
|
a8ad335aed | ||
|
|
7022a80c95 | ||
|
|
9a2f602d6c | ||
|
|
c78a4aa241 | ||
|
|
975ce4d973 | ||
|
|
1e6ff0d969 | ||
|
|
69e133d561 |
13
.travis.yml
13
.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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
||||
[](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade)
|
||||
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://twitter.com/Cryptomator)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>ant-kit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>commons-test</artifactId>
|
||||
<name>Cryptomator common test dependencies</name>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator common</name>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerThrowingException<T, E extends Exception> {
|
||||
public interface ConsumerThrowingException<T, E extends Throwable> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Exception> {
|
||||
public interface RunnableThrowingException<T extends Throwable> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
|
||||
@@ -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(')');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SupplierThrowingException<T, E extends Throwable> {
|
||||
|
||||
T get() throws E;
|
||||
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
<name>Cryptomator filesystem: API</name>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-charsets</artifactId>
|
||||
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer tests</name>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer</name>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
<sivmode.version>1.0.4</sivmode.version>
|
||||
<sivmode.version>1.0.7</sivmode.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -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<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<File> nonConflictingFiles() {
|
||||
if (exists()) {
|
||||
final Stream<? extends File> 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<File> containsEncryptedName() {
|
||||
private Predicate<File> 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<String> decryptChildName(String ciphertextFileName) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
<name>Cryptomator filesystem: In-memory mock</name>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-invariants-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Invariants tests</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nameshortening</artifactId>
|
||||
<name>Cryptomator filesystem: Name shortening layer</name>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nio</artifactId>
|
||||
<name>Cryptomator filesystem: NIO-based physical layer</name>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Runnable> 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<Runnable> 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<Runnable> 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<Runnable> 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();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-stats</artifactId>
|
||||
<name>Cryptomator filesystem: Throughput statistics</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>frontend-api</artifactId>
|
||||
<name>Cryptomator frontend: API</name>
|
||||
|
||||
@@ -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<MountParam, Optional<String>> 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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>frontend-webdav</artifactId>
|
||||
<name>Cryptomator frontend: WebDAV frontend</name>
|
||||
|
||||
@@ -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<FrontendId> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<FrontendId> 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> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<MountParam, Optional<String>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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<Runnable> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,6 +40,11 @@ import com.google.common.io.ByteStreams;
|
||||
class DavFile extends DavNode<FileLocator> {
|
||||
|
||||
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<FileLocator> {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, ActiveLock> lockMap = Objects.requireNonNull(lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()));
|
||||
return lockMap.computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
|
||||
}
|
||||
|
||||
private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam;
|
||||
final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MountParam, Optional<String>> mountParams) {
|
||||
if (SystemUtils.IS_OS_LINUX) {
|
||||
Optional<String> 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<MountParam, Optional<String>> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,12 +30,14 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
if (SystemUtils.IS_OS_LINUX) {
|
||||
Optional<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<WebDavMounterStrategy> {
|
||||
|
||||
private final Collection<WebDavMounterStrategy> 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<WebDavMounterStrategy> iterator() {
|
||||
return delegate.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
return delegate.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> 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<? extends WebDavMounterStrategy> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<WebDavMounterStrategy> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<WebDavMounter> {
|
||||
public class WebDavMounterProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
|
||||
private final WebDavMounterStrategy choosenStrategy;
|
||||
private final Collection<WebDavMounterStrategy> availableStrategies;
|
||||
private final WebDavMounterStrategy fallbackStrategy;
|
||||
|
||||
@Inject
|
||||
public WebDavMounterProvider(MountStrategies availableStrategies) {
|
||||
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
|
||||
public WebDavMounterProvider(Set<WebDavMounterStrategy> availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) {
|
||||
this.availableStrategies = availableStrategies;
|
||||
this.fallbackStrategy = fallbackStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMounter get() {
|
||||
return this.choosenStrategy;
|
||||
}
|
||||
|
||||
private WebDavMounterStrategy getStrategyWhichShouldWork(Collection<WebDavMounterStrategy> availableStrategies) {
|
||||
WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter());
|
||||
LOG.info("Using {}", strategy.getClass().getSimpleName());
|
||||
return strategy;
|
||||
public WebDavMounter chooseMounter(Map<MountParam, Optional<String>> mountParams) {
|
||||
WebDavMounterStrategy result = availableStrategies.stream().filter(strategy -> strategy.shouldWork(mountParams)).findFirst().orElse(fallbackStrategy);
|
||||
LOG.info("Using {}", result.getClass().getSimpleName());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<MountParam, Optional<String>> mountParams);
|
||||
|
||||
/**
|
||||
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
|
||||
|
||||
@@ -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<Character> A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet());
|
||||
|
||||
|
||||
private static final Set<Character> 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<Character> 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<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
|
||||
}
|
||||
|
||||
|
||||
public Set<Character> getAvailableDriveLetters() {
|
||||
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> 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<String> overrides = new HashSet<>();
|
||||
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
|
||||
@@ -122,7 +122,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
overrides.add("<local>");
|
||||
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
|
||||
|
||||
@@ -61,13 +61,18 @@ final class CommandRunner {
|
||||
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
|
||||
try {
|
||||
final List<String> 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>jacoco-report</artifactId>
|
||||
<name>Cryptomator Code Coverage Report</name>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>uber-jar</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<version>1.1.4</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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;
|
||||
|
||||
@@ -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> frontendFactory;
|
||||
private final Settings settings;
|
||||
private final WindowsDriveLetters driveLetters;
|
||||
@@ -65,10 +65,10 @@ public class UnlockController extends LocalizedFXMLViewController {
|
||||
private Optional<UnlockListener> listener = Optional.empty();
|
||||
|
||||
@Inject
|
||||
public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
|
||||
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> 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) {
|
||||
|
||||
@@ -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> vault = new SimpleObjectProperty<>();
|
||||
private Optional<LockListener> listener = Optional.empty();
|
||||
private Timeline ioAnimation;
|
||||
|
||||
@Inject
|
||||
public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, ExecutorService exec) {
|
||||
public UnlockedController(Localization localization, Provider<MacWarningsController> 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);
|
||||
|
||||
@@ -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> vault = new SimpleObjectProperty<>();
|
||||
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
|
||||
private final UpgradeStrategies strategies;
|
||||
private final ExecutorService exec;
|
||||
private final AsyncTaskService asyncTaskService;
|
||||
private Optional<UpgradeListener> 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() {
|
||||
|
||||
@@ -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<String> semVerComparator;
|
||||
private final ExecutorService executor;
|
||||
private final AsyncTaskService asyncTaskService;
|
||||
|
||||
@Inject
|
||||
public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
|
||||
public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> 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<String> applicationVersion() {
|
||||
@@ -161,7 +154,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
|
||||
|
||||
@FXML
|
||||
public void didClickUpdateLink(ActionEvent event) {
|
||||
app.getHostServices().showDocument("https://cryptomator.org/#download");
|
||||
app.getHostServices().showDocument("https://cryptomator.org/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.cryptomator.ui.model;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -14,22 +17,40 @@ import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.crypto.Constants;
|
||||
import org.cryptomator.ui.settings.Localization;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Contains the collective knowledge of all creatures who were alive during the development of vault format 3.
|
||||
* This class uses no external classes from the crypto or shortening layer by purpose, so we don't need legacy code inside these.
|
||||
*/
|
||||
@Singleton
|
||||
class UpgradeVersion3to4 extends UpgradeStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
|
||||
private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_");
|
||||
private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all)
|
||||
private static final String LONG_FILENAME_SUFFIX = ".lng";
|
||||
private static final String OLD_FOLDER_SUFFIX = "_";
|
||||
private static final String NEW_FOLDER_PREFIX = "0";
|
||||
|
||||
private final MessageDigest sha1;
|
||||
private final BaseNCodec base32 = new Base32();
|
||||
|
||||
@Inject
|
||||
public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
|
||||
super(cryptorProvider, localization);
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("SHA-1 exists in every JVM");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -40,6 +61,7 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
|
||||
@Override
|
||||
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
|
||||
Path dataDir = vault.path().get().resolve("d");
|
||||
Path metadataDir = vault.path().get().resolve("m");
|
||||
if (!Files.isDirectory(dataDir)) {
|
||||
return; // empty vault. no migration needed.
|
||||
}
|
||||
@@ -53,7 +75,12 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
migrate(file, attrs);
|
||||
String name = file.getFileName().toString();
|
||||
if (name.endsWith(LONG_FILENAME_SUFFIX)) {
|
||||
migrateLong(metadataDir, file);
|
||||
} else {
|
||||
migrate(file, attrs);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@@ -82,7 +109,7 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
|
||||
if (m.find(0) && size < FILE_MIN_SIZE) {
|
||||
String base32 = m.group(1);
|
||||
String suffix = name.substring(m.end());
|
||||
String renamed = "0" + base32 + (suffix.isEmpty() ? "" : " " + suffix);
|
||||
String renamed = NEW_FOLDER_PREFIX + base32 + (suffix.isEmpty() ? "" : " " + suffix);
|
||||
renameWithoutOverwriting(file, renamed);
|
||||
}
|
||||
}
|
||||
@@ -96,12 +123,33 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
|
||||
LOG.info("Renaming {} to {}", path, newPath.getFileName());
|
||||
}
|
||||
|
||||
private void migrateLong(Path metadataDir, Path path) throws IOException {
|
||||
String oldName = path.getFileName().toString();
|
||||
Path oldMetadataFile = metadataDir.resolve(oldName.substring(0, 2)).resolve(oldName.substring(2, 4)).resolve(oldName);
|
||||
if (Files.isRegularFile(oldMetadataFile)) {
|
||||
String oldContent = new String(Files.readAllBytes(oldMetadataFile), UTF_8);
|
||||
if (oldContent.endsWith(OLD_FOLDER_SUFFIX)) {
|
||||
String newContent = NEW_FOLDER_PREFIX + StringUtils.removeEnd(oldContent, OLD_FOLDER_SUFFIX);
|
||||
String newName = base32.encodeAsString(sha1.digest(newContent.getBytes(UTF_8))) + LONG_FILENAME_SUFFIX;
|
||||
Path newPath = path.resolveSibling(newName);
|
||||
Path newMetadataFile = metadataDir.resolve(newName.substring(0, 2)).resolve(newName.substring(2, 4)).resolve(newName);
|
||||
Files.move(path, newPath);
|
||||
Files.createDirectories(newMetadataFile.getParent());
|
||||
Files.write(newMetadataFile, newContent.getBytes(UTF_8));
|
||||
Files.delete(oldMetadataFile);
|
||||
LOG.info("Renaming {} to {}\nDeleting {}\nCreating {}", path, newName, oldMetadataFile, newMetadataFile);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Found uninflatable long file name. Expected: {}", oldMetadataFile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Vault vault) {
|
||||
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
|
||||
try {
|
||||
if (Files.isRegularFile(masterkeyFile)) {
|
||||
final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
|
||||
final String keyContents = new String(Files.readAllBytes(masterkeyFile), UTF_8);
|
||||
return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
|
||||
} else {
|
||||
LOG.warn("Not a file: {}", masterkeyFile);
|
||||
|
||||
@@ -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<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
|
||||
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
|
||||
private final AtomicReference<FileSystem> 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<Path>(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
|
||||
// *******************************************************************************/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
|
||||
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<ObjectMapper> {
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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<Settings> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Void> asyncTaskOf(RunnableThrowingException<?> task) {
|
||||
return new AsyncTaskImpl<>(() -> {
|
||||
task.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public <ResultType> AsyncTaskWithoutSuccessHandler<ResultType> asyncTaskOf(SupplierThrowingException<ResultType, ?> task) {
|
||||
return new AsyncTaskImpl<>(task);
|
||||
}
|
||||
|
||||
private class AsyncTaskImpl<ResultType> implements AsyncTaskWithoutSuccessHandler<ResultType> {
|
||||
|
||||
private final SupplierThrowingException<ResultType, ?> task;
|
||||
|
||||
private ConsumerThrowingException<ResultType, ?> successHandler = value -> {
|
||||
};
|
||||
private List<ErrorHandler<Throwable>> errorHandlers = new ArrayList<>();
|
||||
private RunnableThrowingException<?> finallyHandler = () -> {
|
||||
};
|
||||
|
||||
public AsyncTaskImpl(SupplierThrowingException<ResultType, ?> task) {
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler) {
|
||||
successHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler) {
|
||||
return onSuccess(result -> handler.run());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Override
|
||||
public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler) {
|
||||
errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> 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<Throwable> errorHandler = errorHandlerFor(e);
|
||||
Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e)));
|
||||
} finally {
|
||||
Platform.runLater(toRunnableLoggingException(finallyHandler));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private ErrorHandler<Throwable> 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<ErrorType> implements ConsumerThrowingException<ErrorType, Throwable> {
|
||||
|
||||
public static final ErrorHandler<Throwable> LOGGING_HANDLER = new ErrorHandler<Throwable>(Throwable.class, error -> {
|
||||
LOG.error("Uncaught exception", error);
|
||||
});
|
||||
|
||||
private final Class<ErrorType> type;
|
||||
private final ConsumerThrowingException<ErrorType, ?> delegate;
|
||||
|
||||
public ErrorHandler(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> 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<ResultType> extends AsyncTaskWithoutErrorHandler {
|
||||
|
||||
AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler);
|
||||
|
||||
AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler {
|
||||
|
||||
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
|
||||
|
||||
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTaskWithoutFinallyHandler extends AsyncTask {
|
||||
|
||||
AsyncTask andFinally(RunnableThrowingException<?> handler);
|
||||
|
||||
}
|
||||
|
||||
public interface AsyncTask extends Runnable {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,12 +28,6 @@ public interface DeferredClosable<T> extends AutoCloseable {
|
||||
*/
|
||||
public Optional<T> get();
|
||||
|
||||
/**
|
||||
* Quietly closes the Object. If the object was closed before, nothing
|
||||
* happens.
|
||||
*/
|
||||
public void close();
|
||||
|
||||
/**
|
||||
* @return an empty object.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
*
|
||||
* <p>
|
||||
* 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<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
|
||||
|
||||
@@ -51,33 +47,32 @@ public class DeferredCloser implements AutoCloseable {
|
||||
final AtomicLong counter = new AtomicLong();
|
||||
|
||||
private class ManagedResource<T> implements DeferredClosable<T> {
|
||||
|
||||
private final long number = counter.incrementAndGet();
|
||||
|
||||
private final AtomicReference<T> object = new AtomicReference<>();
|
||||
private final T object;
|
||||
private final ConsumerThrowingException<T, Exception> closer;
|
||||
private boolean closed = false;
|
||||
|
||||
public ManagedResource(T object, ConsumerThrowingException<T, Exception> 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<T> 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<ManagedResource<?>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.ChoiceBox?>
|
||||
|
||||
<VBox prefWidth="400.0" alignment="TOP_CENTER" spacing="12.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
|
||||
<Label VBox.vgrow="NEVER" fx:id="versionLabel" alignment="CENTER" cacheShape="true" cache="true" />
|
||||
@@ -40,6 +41,11 @@
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" fx:id="useIpv6Label" text="%settings.useipv6.label" cacheShape="true" cache="true" />
|
||||
<CheckBox GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="useIpv6Checkbox" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" fx:id="prefGvfsSchemeLabel" text="%settings.prefGvfsScheme.label" cacheShape="true" cache="true" />
|
||||
<ChoiceBox GridPane.rowIndex="3" GridPane.columnIndex="1" fx:id="prefGvfsScheme" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
</children>
|
||||
</GridPane>
|
||||
<Label VBox.vgrow="NEVER" text="%settings.requiresRestartLabel" alignment="CENTER" cacheShape="true" cache="true" />
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
|
||||
<VBox prefWidth="400.0" prefHeight="400.0" spacing="24.0" alignment="CENTER" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
|
||||
|
||||
<VBox fx:id="checkForUpdatesContainer" spacing="6.0" alignment="CENTER" cacheShape="true" cache="true" prefHeight="50.0">
|
||||
<VBox fx:id="checkForUpdatesContainer" spacing="6.0" alignment="CENTER" cacheShape="true" cache="true" prefHeight="64.0">
|
||||
<HBox alignment="CENTER" spacing="5.0" cacheShape="true" cache="true">
|
||||
<Label fx:id="checkForUpdatesStatus" cacheShape="true" cache="true" />
|
||||
<ProgressIndicator fx:id="checkForUpdatesIndicator" progress="-1" prefWidth="15.0" prefHeight="15.0" cacheShape="true" cache="true" cacheHint="SPEED" />
|
||||
</HBox>
|
||||
<Hyperlink alignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" cacheShape="true" cache="true" disable="true" />
|
||||
<Hyperlink wrapText="true" textAlignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" cacheShape="true" cache="true" disable="true" />
|
||||
</VBox>
|
||||
|
||||
<ImageView fitHeight="200.0" preserveRatio="true" smooth="false" cache="true" style="-fx-background-color: green;">
|
||||
<ImageView fitHeight="200.0" preserveRatio="true" smooth="true" cache="true" style="-fx-background-color: green;">
|
||||
<Image url="/bot_welcome.png"/>
|
||||
</ImageView>
|
||||
|
||||
<VBox prefHeight="50.0"/>
|
||||
<VBox prefHeight="64.0"/>
|
||||
|
||||
</VBox>
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Tresor erstellen
|
||||
main.addDirectory.contextMenu.open = Tresor öffnen
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Prüfe auf Updates...
|
||||
welcome.newVersionMessage = Version %1$s kann heruntergeladen werden. Momentane Version %2$s.
|
||||
welcome.newVersionMessage = Version %1$s kann heruntergeladen werden.\nMomentane Version %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Passwort
|
||||
initialize.label.retypePassword = Passwort bestätigen
|
||||
|
||||
@@ -19,7 +19,7 @@ main.directoryList.remove.confirmation.content=The vault will only be removed fr
|
||||
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking=Checking for Updates...
|
||||
welcome.newVersionMessage=Version %1$s can be downloaded. This is %2$s.
|
||||
welcome.newVersionMessage=Version %1$s can be downloaded.\nThis is %2$s.
|
||||
|
||||
# initialize.fxml
|
||||
initialize.label.password=Password
|
||||
@@ -94,6 +94,7 @@ settings.checkForUpdates.label=Check for updates
|
||||
settings.port.label=WebDAV Port *
|
||||
settings.port.prompt=0 = Choose automatically
|
||||
settings.useipv6.label=Use IPv6 literal
|
||||
settings.prefGvfsScheme.label=WebDAV scheme
|
||||
settings.requiresRestartLabel=* Cryptomator needs to restart
|
||||
|
||||
# tray icon
|
||||
|
||||
@@ -8,7 +8,7 @@ main.addDirectory.contextMenu.new = Crear una nueva caja fuerte
|
||||
main.addDirectory.contextMenu.open = Abrir una caja fuerte existente
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Chequando por actualizaciónes...
|
||||
welcome.newVersionMessage = Se puede bajar version %1$s. Este es %2$s.
|
||||
welcome.newVersionMessage = Se puede bajar version %1$s.\nEste es %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Contraseña
|
||||
initialize.label.retypePassword = Reintroduzca contraseña
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Créer un nouveau coffre
|
||||
main.addDirectory.contextMenu.open = Ouvrir un coffre existant
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Recherche de mise à jour...
|
||||
welcome.newVersionMessage = La version %1$s peut-être téléchargée. Il s'agit de %2$s.
|
||||
welcome.newVersionMessage = La version %1$s peut-être téléchargée.\nIl s'agit de %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Mot de passe
|
||||
initialize.label.retypePassword = Confirmation
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Új széf létrehozása
|
||||
main.addDirectory.contextMenu.open = Létező széf megnyitása
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Frissítések keresése...
|
||||
welcome.newVersionMessage = Új verzió érhető el\: %1$s. Jelenlegi verzió\: %2$s.
|
||||
welcome.newVersionMessage = Új verzió érhető el\: %1$s.\nJelenlegi verzió\: %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Jelszó
|
||||
initialize.label.retypePassword = Jelszó ismét
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Crea un nuovo vault
|
||||
main.addDirectory.contextMenu.open = Apri un vault
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Verifica aggiornamenti...
|
||||
welcome.newVersionMessage = La versione %1$s può essere scaricata. Questa è %2$s
|
||||
welcome.newVersionMessage = La versione %1$s può essere scaricata.\nQuesta è %2$s
|
||||
# initialize.fxml
|
||||
initialize.label.password = Password
|
||||
initialize.label.retypePassword = Conferma password
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = 새 보관함 만들기
|
||||
main.addDirectory.contextMenu.open = 기존 보관함 열기
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = 업데이트 확인 중...
|
||||
welcome.newVersionMessage = %1$s 버전이 새로 다운로드 가능합니다. 지금 버전은 %2$s 입니다.
|
||||
welcome.newVersionMessage = %1$s 버전이 새로 다운로드 가능합니다.\n지금 버전은 %2$s 입니다.
|
||||
# initialize.fxml
|
||||
initialize.label.password = 비밀번호
|
||||
initialize.label.retypePassword = 비밀번호 재입력
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Creeer nieuwe kluis
|
||||
main.addDirectory.contextMenu.open = Open bestaande kluis
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Controleren op Updates...
|
||||
welcome.newVersionMessage = Versie %1$s kan worden gedownload. Dit is %2$s.
|
||||
welcome.newVersionMessage = Versie %1$s kan worden gedownload.\nDit is %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Wachtwoord
|
||||
initialize.label.retypePassword = Voer wachtwoord opnieuw in
|
||||
|
||||
@@ -8,7 +8,7 @@ main.addDirectory.contextMenu.open = Открыть имеющееся хран
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Проверка обновлений...
|
||||
# Does the first %s mean the new version number, and the second %s - the current version user has?
|
||||
welcome.newVersionMessage = Доступна версия %1$s. У вас версия %2$s.
|
||||
welcome.newVersionMessage = Доступна версия %1$s.\nУ вас версия %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Пароль
|
||||
initialize.label.retypePassword = Введите пароль ещё раз
|
||||
|
||||
@@ -10,7 +10,7 @@ main.addDirectory.contextMenu.new = Vytvoriť nový trezor
|
||||
main.addDirectory.contextMenu.open = Otvoriť existujúci trezor
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Kontrolujú sa aktualizácie...
|
||||
welcome.newVersionMessage = Verzia %1$s je pripravená na stiahnutie. Toto je verzia %2$s.
|
||||
welcome.newVersionMessage = Verzia %1$s je pripravená na stiahnutie.\nToto je verzia %2$s.
|
||||
# initialize.fxml
|
||||
initialize.label.password = Heslo
|
||||
initialize.label.retypePassword = Zadajte heslo znova
|
||||
|
||||
@@ -7,7 +7,7 @@ main.addDirectory.contextMenu.new = Yeni bir kasa yarat
|
||||
main.addDirectory.contextMenu.open = Var olan kasayı aç
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Güncellemeler kontrol ediliyor...
|
||||
welcome.newVersionMessage = Sürüm %1$s indirilebilir. Şu anki sürüm\: %2$s
|
||||
welcome.newVersionMessage = Sürüm %1$s indirilebilir.\nŞu anki sürüm\: %2$s
|
||||
# initialize.fxml
|
||||
initialize.label.password = Şifre
|
||||
initialize.label.retypePassword = Şifre (tekrar)
|
||||
|
||||
Reference in New Issue
Block a user