first compile-clean attempt to integrate the layered I/O subsystem with the existing UI

This commit is contained in:
Sebastian Stenzel
2016-01-26 20:17:33 +01:00
parent e4d626eef5
commit c56d0b7d4a
42 changed files with 539 additions and 430 deletions
+5 -1
View File
@@ -15,7 +15,7 @@
<version>0.11.0-SNAPSHOT</version>
</parent>
<artifactId>frontend-webdav</artifactId>
<name>Jackrabbit-based WebDAV filesystem frontend</name>
<name>Cryptomator frontend: WebDAV frontend</name>
<description>Provides access via WebDAV to filesystems</description>
<properties>
@@ -32,6 +32,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
</dependency>
<!-- Jackrabbit -->
<dependency>
@@ -1,4 +1,4 @@
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import javax.inject.Singleton;
@@ -0,0 +1,72 @@
package org.cryptomator.frontend.webdav;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import org.cryptomator.frontend.Frontend;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.webdav.mount.CommandFailedException;
import org.cryptomator.frontend.webdav.mount.WebDavMount;
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
import org.eclipse.jetty.servlet.ServletContextHandler;
class WebDavFrontend implements Frontend {
private final WebDavMounterProvider webdavMounterProvider;
private final ServletContextHandler handler;
private final URI uri;
private WebDavMount mount;
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
this.webdavMounterProvider = webdavMounterProvider;
this.handler = handler;
this.uri = uri;
try {
handler.start();
} catch (Exception e) {
throw new FrontendCreationFailedException(e);
}
}
@Override
public void close() throws Exception {
unmount();
handler.stop();
}
@Override
public boolean mount(Map<MountParam, Optional<String>> mountParams) {
try {
mount = webdavMounterProvider.get().mount(uri, mountParams);
return true;
} catch (CommandFailedException e) {
return false;
}
}
@Override
public void unmount() {
if (mount != null) {
try {
mount.unmount();
} catch (CommandFailedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void reveal() {
if (mount != null) {
try {
mount.reveal();
} catch (CommandFailedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
@@ -1,4 +1,4 @@
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import java.net.URI;
import java.net.URISyntaxException;
@@ -10,6 +10,10 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.frontend.Frontend;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -21,7 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class WebDavServer {
public class WebDavServer implements FrontendFactory {
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
@@ -34,15 +38,17 @@ public class WebDavServer {
private final ServerConnector localConnector;
private final ContextHandlerCollection servletCollection;
private final WebDavServletContextFactory servletContextFactory;
private final WebDavMounterProvider webdavMounterProvider;
@Inject
WebDavServer(WebDavServletContextFactory servletContextFactory) {
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider) {
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);
this.localConnector = new ServerConnector(server);
this.servletCollection = new ContextHandlerCollection();
this.servletContextFactory = servletContextFactory;
this.webdavMounterProvider = webdavMounterProvider;
localConnector.setHost(LOCALHOST);
server.setConnectors(new Connector[] {localConnector});
@@ -82,17 +88,27 @@ public class WebDavServer {
}
}
public ServletContextHandler addServlet(Folder root, String contextPath) {
// visible for testing
ServletContextHandler addServlet(Folder root, URI contextRoot) {
ServletContextHandler handler = servletContextFactory.create(contextRoot, root);
servletCollection.addHandler(handler);
servletCollection.mapContexts();
return handler;
}
@Override
public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException {
if (!contextPath.startsWith("/")) {
throw new IllegalArgumentException("contextPath must begin with '/'");
}
final URI uri;
try {
uri = new URI("http", null, LOCALHOST, getPort(), contextPath, null, null);
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
ServletContextHandler handler = servletContextFactory.create(uri, root);
servletCollection.addHandler(handler);
servletCollection.mapContexts();
return handler;
final ServletContextHandler handler = addServlet(root, uri);
return new WebDavFrontend(webdavMounterProvider, handler, uri);
}
}
@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import java.net.URI;
import java.util.EnumSet;
@@ -0,0 +1,10 @@
package org.cryptomator.frontend.webdav.mount;
abstract class AbstractWebDavMount implements WebDavMount {
@Override
public void close() throws Exception {
this.unmount();
}
}
@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* 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
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
public class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
public CommandFailedException(String message) {
super(message);
}
public CommandFailedException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,63 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import org.cryptomator.frontend.Frontend.MountParam;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
* @author Markus Kreusch
*/
final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return true;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) {
displayMountInstructions();
return new AbstractWebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();
}
@Override
public void reveal() throws CommandFailedException {
displayRevealInstructions();
}
};
}
private void displayMountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do
// Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-)
}
private void displayUnmountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
}
private void displayRevealInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#reveal which describes what to do
}
}
@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (c) 2014 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.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Inject
LinuxGvfsWebDavMounter() {
}
@Override
public boolean shouldWork() {
if (SystemUtils.IS_OS_LINUX) {
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
try {
checkScripts.execute();
return true;
} 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 LinuxGvfsWebDavMount(uri);
}
private static class LinuxGvfsWebDavMount extends AbstractWebDavMount {
private final URI webDavUri;
private final Script testMountStillExistsScript;
private final Script unmountScript;
private LinuxGvfsWebDavMount(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 {
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);
}
}
}
@@ -0,0 +1,83 @@
/*******************************************************************************
* Copyright (c) 2014 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Inject
MacOsXWebDavMounter() {
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String mountName = mountParams.get(MountParam.MOUNT_NAME).orElseThrow(() -> {
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
});
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines("mkdir \"$MOUNT_PATH\"", "mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"").addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath()).addEnv("MOUNT_PATH", path).addEnv("MOUNT_NAME", mountName);
mountScript.execute();
return new MacWebDavMount(path);
}
private static class MacWebDavMount extends AbstractWebDavMount {
private final String mountPath;
private final Script revealScript;
private final Script unmountScript;
private MacWebDavMount(String mountPath) {
this.mountPath = mountPath;
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
revealScript.execute();
}
}
}
@@ -0,0 +1,84 @@
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, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public Iterator<WebDavMounterStrategy> iterator() {
return delegate.iterator();
}
public Object[] toArray() {
return delegate.toArray();
}
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public boolean add(WebDavMounterStrategy e) {
return delegate.add(e);
}
public boolean remove(Object o) {
return delegate.remove(o);
}
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
return delegate.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public void clear() {
delegate.clear();
}
public boolean equals(Object o) {
return delegate.equals(o);
}
public int hashCode() {
return delegate.hashCode();
}
}
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount extends AutoCloseable {
/**
* Unmounts this {@code WebDavMount}.
*
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
/**
* Reveals the mounted drive in the operating systems default file browser.
*
* @throws CommandFailedException if the reveal operation fails
*/
void reveal() throws CommandFailedException;
}
@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* 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 to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import org.cryptomator.frontend.Frontend.MountParam;
public interface WebDavMounter {
/**
* Tries to mount a given webdav share.
*
* @param uri URI of the webdav share
* @param mountParams additional mount parameters, that might not get ignored by some OS-specific mounters.
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
* @throws IllegalArgumentException if mountParams is missing expected options
*/
WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException;
}
@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored to use strategy pattern
* Sebastian Stenzel - Refactored to use Guice provider, added warmup-phase for windows mounts.
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.util.Collection;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class WebDavMounterProvider implements Provider<WebDavMounter> {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
private final WebDavMounterStrategy choosenStrategy;
@Inject
public WebDavMounterProvider(MountStrategies availableStrategies) {
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
}
@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;
}
}
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Sebastian Stenzel - minor strategy fine tuning
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
/**
* A strategy able to mount a webdav share and display it to the user.
*
* @author Markus Kreusch
*/
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();
/**
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
* when certain things are prepared before the actual mount attempt.
*/
void warmUp(int serverPort);
}
@@ -0,0 +1,41 @@
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.StreamSupport;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
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());
@Inject
public WindowsDriveLetters() {
}
public Set<Character> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
}
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());
}
}
@@ -0,0 +1,144 @@
/*******************************************************************************
* Copyright (c) 2014 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.CommandResult;
import org.cryptomator.frontend.webdav.mount.command.Script;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*/
@Singleton
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 8;
private static final char AUTO_ASSIGN_DRIVE_LETTER = '*';
private final WindowsDriveLetters driveLetters;
@Inject
WindowsWebDavMounter(WindowsDriveLetters driveLetters) {
this.driveLetters = driveLetters;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final Character driveLetter = mountParams.get(MountParam.WIN_DRIVE_LETTER).map(CharUtils::toCharacterObject).orElse(AUTO_ASSIGN_DRIVE_LETTER);
if (driveLetters.getOccupiedDriveLetters().contains(driveLetter)) {
throw new CommandFailedException("Drive letter occupied.");
}
final String driveLetterStr = driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? CharUtils.toString(AUTO_ASSIGN_DRIVE_LETTER) : driveLetter + ":";
final Script localhostMountScript = fromLines("net use %DRIVE_LETTER% \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
localhostMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
localhostMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
CommandResult mountResult;
try {
mountResult = localhostMountScript.execute(5, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
final Script ipv6literaltMountScript = fromLines("net use %DRIVE_LETTER% \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
ipv6literaltMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
ipv6literaltMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script proxyBypassScript = fromLines(
"reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
}
return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter);
}
private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException {
CommandFailedException latestException = null;
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
// wait a moment before next attempt
Thread.sleep(5000);
proxyBypassScript.execute();
// alternate localhost and 0--1.ipv6literal.net
final Script mountScript = (i % 2 == 0) ? localhostMountScript : ipv6literalMountScript;
return mountScript.execute(3, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
latestException = ex;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new CommandFailedException(ex);
}
}
throw latestException;
}
private Character getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
return CharUtils.toCharacterObject(matcher.group(1));
} else {
throw new CommandFailedException("Failed to get a drive letter from net use output.");
}
}
private class WindowsWebDavMount extends AbstractWebDavMount {
private final Character driveLetter;
private final Script openExplorerScript;
private final Script unmountScript;
private WindowsWebDavMount(Character driveLetter) {
this.driveLetter = driveLetter;
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter));
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (isVolumeMounted()) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
openExplorerScript.execute();
}
private boolean isVolumeMounted() {
return driveLetters.getOccupiedDriveLetters().contains(driveLetter);
}
}
}
@@ -0,0 +1,107 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - using Futures, lazy loading for out/err.
******************************************************************************/
package org.cryptomator.frontend.webdav.mount.command;
import static java.lang.String.format;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.frontend.webdav.mount.CommandFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CommandResult {
private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class);
private final Process process;
private final String stdout;
private final String stderr;
private final CommandFailedException exception;
/**
* Constructs a CommandResult from a terminated process and closes all its streams.
* @param process An <strong>already finished</strong> process.
*/
CommandResult(Process process) {
String out = null;
String err = null;
CommandFailedException ex = null;
try {
out = IOUtils.toString(process.getInputStream());
err = IOUtils.toString(process.getErrorStream());
} catch (IOException e) {
ex = new CommandFailedException(e);
} finally {
this.process = process;
this.stdout = out;
this.stderr = err;
this.exception = ex;
IOUtils.closeQuietly(process.getInputStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getErrorStream());
logDebugInfo();
}
}
/**
* @return Data written to STDOUT
*/
public String getStdOut() throws CommandFailedException {
assertNoException();
return stdout;
}
/**
* @return Data written to STDERR
*/
public String getStdErr() throws CommandFailedException {
assertNoException();
return stderr;
}
/**
* @return Exit value of the process
*/
public int getExitValue() {
return process.exitValue();
}
private void logDebugInfo() {
if (LOG.isDebugEnabled()) {
if (Strings.isEmpty(stderr) && Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}", process.exitValue());
} else if (Strings.isEmpty(stderr)) {
LOG.debug("Command execution finished. Exit code: {}\nOutput: {}", process.exitValue(), stdout);
} else if (Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}\nError: {}", process.exitValue(), stderr);
} else {
LOG.debug("Command execution finished. Exit code: {}\n Output: {}\nError: {}", process.exitValue(), stdout, stderr);
}
}
}
void assertOk() throws CommandFailedException {
assertNoException();
int exitValue = getExitValue();
if (exitValue != 0) {
throw new CommandFailedException(format("Command execution failed. Exit code: %d\n" + "# Output:\n" + "%s\n" + "# Error:\n" + "%s", exitValue, stdout, stderr));
}
}
private void assertNoException() throws CommandFailedException {
if (exception != null) {
throw exception;
}
}
}
@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - Refactoring
******************************************************************************/
package org.cryptomator.frontend.webdav.mount.command;
import static java.lang.String.format;
import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.webdav.mount.CommandFailedException;
/**
* <p>
* Runs commands using a system compatible CLI.
* <p>
* To detect the system type {@link SystemUtils} is used. The following CLIs are used by default:
* <ul>
* <li><i>{@link #WINDOWS_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_WINDOWS}
* <li><i>{@link #UNIX_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_UNIX}
* </ul>
* <p>
* If the path to the executables differs from the default or the system can not be detected the Java system property
* {@value #CLI_EXECUTABLE_PROPERTY} can be set to define it.
* <p>
* If a CLI executable can not be determined using these methods operation of {@link CommandRunner} will fail with
* {@link IllegalStateException}s.
*
* @author Markus Kreusch
*/
final class CommandRunner {
public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli";
public static final String WINDOWS_DEFAULT_CLI[] = {"cmd", "/C"};
public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-c"};
private static final Executor CMD_EXECUTOR = Executors.newCachedThreadPool();
/**
* Executes all lines in the given script in the specified order. Stops as soon as the first command fails.
*
* @param script Script containing command lines and environment variables.
* @return Result of the last command, if it exited successfully.
* @throws CommandFailedException If one of the command lines in the given script fails.
*/
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());
CommandResult result = null;
for (final String line : script.getLines()) {
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();
}
return result;
} catch (IOException e) {
throw new CommandFailedException(e);
}
}
private static CommandResult run(Process process, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final FutureCommandResult futureCommandResult = new FutureCommandResult(process);
CMD_EXECUTOR.execute(futureCommandResult);
return futureCommandResult.get(timeout, unit);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new CommandFailedException("Waiting time elapsed before command execution finished");
}
}
private static String[] determineCli() {
final String cliFromProperty = System.getProperty(CLI_EXECUTABLE_PROPERTY);
if (cliFromProperty != null) {
return cliFromProperty.split("");
} else if (IS_OS_WINDOWS) {
return WINDOWS_DEFAULT_CLI;
} else if (IS_OS_UNIX) {
return UNIX_DEFAULT_CLI;
} else {
throw new IllegalStateException(format("Failed to determine cli to use. Set Java system property %s to the executable.", CLI_EXECUTABLE_PROPERTY));
}
}
}
@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.frontend.webdav.mount.command;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cryptomator.frontend.webdav.mount.CommandFailedException;
final class FutureCommandResult implements Future<CommandResult>, Runnable {
private final Process process;
private final AtomicBoolean canceled = new AtomicBoolean();
private final AtomicBoolean done = new AtomicBoolean();
private final Lock lock = new ReentrantLock();
private final Condition doneCondition = lock.newCondition();
private CommandFailedException exception;
FutureCommandResult(Process process) {
this.process = process;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (done.get()) {
return false;
} else if (canceled.compareAndSet(false, true)) {
if (mayInterruptIfRunning) {
process.destroyForcibly();
}
}
return true;
}
@Override
public boolean isCancelled() {
return canceled.get();
}
private void setDone() {
lock.lock();
try {
done.set(true);
doneCondition.signalAll();
} finally {
lock.unlock();
}
}
@Override
public boolean isDone() {
return done.get();
}
@Override
public CommandResult get() throws InterruptedException, ExecutionException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await();
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public CommandResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await(timeout, unit);
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public void run() {
try {
process.waitFor();
} catch (InterruptedException e) {
exception = new CommandFailedException(e);
} finally {
setDone();
}
}
}
@@ -0,0 +1,65 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
******************************************************************************/
package org.cryptomator.frontend.webdav.mount.command;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.cryptomator.frontend.webdav.mount.CommandFailedException;
public final class Script {
private static final int DEFAULT_TIMEOUT_MILLISECONDS = 3000;
public static Script fromLines(String... commands) {
return new Script(commands);
}
private final String[] lines;
private final Map<String, String> environment = new HashMap<>();
private Script(String[] lines) {
this.lines = lines;
setEnv(System.getenv());
}
public String[] getLines() {
return lines;
}
public CommandResult execute() throws CommandFailedException {
return CommandRunner.execute(this, DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
}
public CommandResult execute(long timeout, TimeUnit unit) throws CommandFailedException {
return CommandRunner.execute(this, timeout, unit);
}
Map<String, String> environment() {
return environment;
}
public Script setEnv(Map<String, String> environment) {
this.environment.clear();
addEnv(environment);
return this;
}
public Script addEnv(Map<String, String> environment) {
this.environment.putAll(environment);
return this;
}
public Script addEnv(String name, String value) {
environment.put(name, value);
return this;
}
}
@@ -6,8 +6,9 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.EnumSet;
@@ -27,7 +28,7 @@ public class InMemoryWebDavServer {
server.start();
FileSystem fileSystem = setupFilesystem();
ServletContextHandler servlet = server.addServlet(fileSystem, "/foo");
ServletContextHandler servlet = server.addServlet(fileSystem, URI.create("http://localhost:8080/foo"));
servlet.addFilter(LoggingHttpFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servlet.start();
@@ -6,12 +6,13 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -36,7 +37,7 @@ public class NioWebDavServer {
server.start();
FileSystem fileSystem = setupFilesystem();
ServletContextHandler servlet = server.addServlet(fileSystem, "/foo");
ServletContextHandler servlet = server.addServlet(fileSystem, URI.create("http://localhost:8080/foo"));
servlet.addFilter(LoggingHttpFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servlet.start();
@@ -1,9 +1,10 @@
package org.cryptomator.webdav.server;
package org.cryptomator.frontend.webdav;
import static org.hamcrest.collection.IsArrayContaining.hasItemInArray;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
@@ -49,7 +50,7 @@ public class WebDavServerTest {
@Before
public void startServlet() throws Exception {
fs = new InMemoryFileSystem();
servlet = SERVER.addServlet(fs, "/test");
servlet = SERVER.addServlet(fs, URI.create("http://localhost:" + SERVER.getPort() + "/test"));
servlet.start();
servletRoot = "http://localhost:" + SERVER.getPort() + "/test";
}