mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f32e4ee4b | ||
|
|
be5cf287c8 | ||
|
|
71892108b3 | ||
|
|
1770bab699 | ||
|
|
1d05e878ab | ||
|
|
f76091ddc0 | ||
|
|
6dff296872 | ||
|
|
6d98442f7e | ||
|
|
3cdda99c67 | ||
|
|
6b45d62aa1 | ||
|
|
b7f3f00ce2 | ||
|
|
dbadf54893 | ||
|
|
38a0cfb2eb | ||
|
|
7d6d061d95 | ||
|
|
c743fa8bdc | ||
|
|
8c2fe14e41 | ||
|
|
ac4f10ce93 | ||
|
|
4f15645bf9 | ||
|
|
c1f4ab6ada | ||
|
|
fd54393f36 | ||
|
|
a2c3b38a75 | ||
|
|
2fb35c59d4 | ||
|
|
afc62656bf | ||
|
|
9c8e4fbf3b | ||
|
|
470a609938 | ||
|
|
863b2ec423 | ||
|
|
d0a420d6c0 | ||
|
|
51e2e94ca9 | ||
|
|
d7efd7fc2f | ||
|
|
db36cfa22e | ||
|
|
cc15f2cdb4 | ||
|
|
b6546f24d5 | ||
|
|
5fe54634a9 | ||
|
|
2fdf9be017 | ||
|
|
1de2d9d2da | ||
|
|
3a5917ef53 | ||
|
|
d0f0c09585 | ||
|
|
884b894e04 | ||
|
|
ebb3207854 | ||
|
|
8abd5ebc01 | ||
|
|
ce197b3314 | ||
|
|
8ae7e95c41 | ||
|
|
6830861346 | ||
|
|
696b3412f2 | ||
|
|
e7ba6f5c92 | ||
|
|
8031b0c516 | ||
|
|
047e1fe1d6 | ||
|
|
75f49b88d6 | ||
|
|
891e79cdae | ||
|
|
b2f20f9a15 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@
|
||||
.project
|
||||
.classpath
|
||||
target/
|
||||
test-output/
|
||||
|
||||
13
README.md
13
README.md
@@ -3,6 +3,8 @@ Cryptomator
|
||||
|
||||
Multiplatform transparent client-side encryption of your files in the cloud. You need Java 8 in order to run the application. Get the runtime environment here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
|
||||
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.jar).
|
||||
|
||||
## Features
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
|
||||
@@ -17,7 +19,6 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
|
||||
## Security
|
||||
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
|
||||
- PBKDF2 key generation
|
||||
- 4096 bit internal masterkey
|
||||
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
|
||||
- Sensitive data is swiped from the heap asap
|
||||
- Lightweight: Complexity kills security
|
||||
@@ -28,17 +29,13 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
|
||||
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
|
||||
|
||||
## Dependencies
|
||||
- Java 8 (for UI only - runs headless on Java 7)
|
||||
- Maven
|
||||
- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...)
|
||||
- Java 8
|
||||
- see pom.xml ;-)
|
||||
|
||||
## TODO
|
||||
|
||||
### Core
|
||||
- Support for HTTP range requests
|
||||
|
||||
### UI
|
||||
- Automount of WebDAV volumes for Win/Tux
|
||||
- Native L&F
|
||||
- Drive icons in WebDAV volumes
|
||||
- Change password functionality
|
||||
- Better explanations on UI
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator core I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.1.0.v20131115</jetty.version>
|
||||
<jetty.version>9.2.5.v20141112</jetty.version>
|
||||
<jackrabbit.version>2.9.0</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
@@ -30,12 +30,6 @@
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty (Servlet Container) -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
@@ -69,20 +63,4 @@
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.cryptomator.files;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
|
||||
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
|
||||
|
||||
private final Path rootDir;
|
||||
private final Cryptor cryptor;
|
||||
private final EncryptionDecider encryptionDecider;
|
||||
private Path currentDir;
|
||||
|
||||
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
|
||||
this.rootDir = rootDir;
|
||||
this.cryptor = cryptor;
|
||||
this.encryptionDecider = encryptionDecider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
|
||||
this.currentDir = dir;
|
||||
return FileVisitResult.CONTINUE;
|
||||
} else {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
|
||||
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
|
||||
final String plaintextName = plaintextFile.getFileName().toString();
|
||||
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
|
||||
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
|
||||
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
|
||||
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.encryptFile(plaintextIn, ciphertextOut);
|
||||
Files.delete(plaintextFile);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
if (encryptionDecider.shouldEncrypt(dir)) {
|
||||
final String plaintext = dir.getFileName().toString();
|
||||
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
|
||||
final Path newPath = dir.resolveSibling(encrypted);
|
||||
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
|
||||
final Path path = currentDir.resolve(metadataFile);
|
||||
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
|
||||
final Path path = currentDir.resolve(metadataFile);
|
||||
return Files.readAllBytes(path);
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
public interface EncryptionDecider {
|
||||
boolean shouldEncrypt(Path path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
@@ -15,68 +18,79 @@ import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class WebDAVServer {
|
||||
public final class WebDavServer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
|
||||
private static final WebDAVServer INSTANCE = new WebDAVServer();
|
||||
private static final String LOCALHOST = "127.0.0.1";
|
||||
private final Server server = new Server();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
|
||||
private static final String LOCALHOST = "::1";
|
||||
private static final int MAX_PENDING_REQUESTS = 200;
|
||||
private static final int MAX_THREADS = 200;
|
||||
private static final int MIN_THREADS = 4;
|
||||
private static final int THREAD_IDLE_SECONDS = 20;
|
||||
private final Server server;
|
||||
private int port;
|
||||
|
||||
private WebDAVServer() {
|
||||
// make constructor private
|
||||
}
|
||||
|
||||
public static WebDAVServer getInstance() {
|
||||
return INSTANCE;
|
||||
public WebDavServer() {
|
||||
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
|
||||
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
||||
server = new Server(tp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param workDir Path of encrypted folder.
|
||||
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
|
||||
* @return port, on which the server did start
|
||||
* @return <code>true</code> upon success
|
||||
*/
|
||||
public int start(final String workDir, final Cryptor cryptor) {
|
||||
public synchronized boolean start(final String workDir, final Cryptor cryptor) {
|
||||
final ServerConnector connector = new ServerConnector(server);
|
||||
connector.setHost(LOCALHOST);
|
||||
|
||||
final String contextPath = "/";
|
||||
final String servletPathSpec = "/*";
|
||||
|
||||
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
|
||||
context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*");
|
||||
context.addServlet(getWebDavServletHolder(workDir, contextPath, cryptor), servletPathSpec);
|
||||
context.setContextPath(contextPath);
|
||||
server.setHandler(context);
|
||||
|
||||
try {
|
||||
server.setConnectors(new Connector[] {connector});
|
||||
server.start();
|
||||
port = connector.getLocalPort();
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Server couldn't be started", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return connector.getLocalPort();
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return server.isRunning();
|
||||
}
|
||||
|
||||
public boolean stop() {
|
||||
public synchronized boolean stop() {
|
||||
try {
|
||||
server.stop();
|
||||
port = 0;
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Server couldn't be stopped", ex);
|
||||
}
|
||||
return server.isStopped();
|
||||
}
|
||||
|
||||
private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
|
||||
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
|
||||
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
|
||||
abstract class AbstractSessionAwareWebDavResourceFactory implements DavResourceFactory {
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
final DavSession session = request.getDavSession();
|
||||
if (session != null && session instanceof WebDavSession) {
|
||||
return createDavResource(locator, (WebDavSession) session, request, response);
|
||||
} else {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session, DavServletRequest request, DavServletResponse response) throws DavException;
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
if (session != null && session instanceof WebDavSession) {
|
||||
return createDavResource(locator, (WebDavSession) session);
|
||||
} else {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session);
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap;
|
||||
|
||||
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
public BidiLRUMap(int maxSize) {
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,26 +8,30 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.SensitiveDataSwipeListener;
|
||||
|
||||
public class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener {
|
||||
class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 10000;
|
||||
private final Path fsRoot;
|
||||
private final Cryptor cryptor;
|
||||
private final BidiLRUMap<String, String> pathCache; // <decryptedPath, encryptedPath>
|
||||
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
|
||||
WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
|
||||
super(httpRoot);
|
||||
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
|
||||
this.cryptor = cryptor;
|
||||
this.pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS);
|
||||
cryptor.addSensitiveDataSwipeListener(this);
|
||||
}
|
||||
|
||||
@@ -48,7 +52,7 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
|
||||
if (resourcePath == null) {
|
||||
return fsRoot.toString();
|
||||
}
|
||||
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/');
|
||||
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return fsRoot.resolve(encryptedRepoPath).toString();
|
||||
}
|
||||
|
||||
@@ -71,7 +75,7 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
|
||||
return null;
|
||||
} else {
|
||||
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
|
||||
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/');
|
||||
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return resourcePath;
|
||||
}
|
||||
}
|
||||
@@ -93,4 +97,22 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
|
||||
pathCache.clear();
|
||||
}
|
||||
|
||||
/* Cryptor I/O Support */
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
if (!Files.isReadable(metaDataFile)) {
|
||||
return null;
|
||||
} else {
|
||||
return Files.readAllBytes(metaDataFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavMethods;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
@@ -24,28 +25,32 @@ import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.PathUtils;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class WebDavResourceFactory implements DavResourceFactory {
|
||||
class WebDavResourceFactory implements DavResourceFactory {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
|
||||
public WebDavResourceFactory(Cryptor cryptor) {
|
||||
WebDavResourceFactory(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
final Path path = PathUtils.getPhysicalPath(locator);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
|
||||
if (Files.exists(path)) {
|
||||
return createResource(locator, request.getDavSession());
|
||||
} else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession());
|
||||
} else if (DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request);
|
||||
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession());
|
||||
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession());
|
||||
} else {
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
@@ -53,17 +58,21 @@ public class WebDavResourceFactory implements DavResourceFactory {
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path path = PathUtils.getPhysicalPath(locator);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
|
||||
if (Files.isDirectory(path)) {
|
||||
return createDirectory(locator, session);
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
if (Files.isRegularFile(path)) {
|
||||
return createFile(locator, session);
|
||||
} else if (Files.isDirectory(path)) {
|
||||
return createDirectory(locator, session);
|
||||
} else {
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
|
||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,15 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
|
||||
public class WebDavSession implements DavSession {
|
||||
class WebDavSession implements DavSession {
|
||||
|
||||
private final WebdavRequest request;
|
||||
|
||||
WebDavSession(WebdavRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addReference(Object reference) {
|
||||
@@ -42,4 +49,8 @@ public class WebDavSession implements DavSession {
|
||||
|
||||
}
|
||||
|
||||
public WebdavRequest getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
|
||||
public class WebDavSessionProvider implements DavSessionProvider {
|
||||
class WebDavSessionProvider implements DavSessionProvider {
|
||||
|
||||
@Override
|
||||
public boolean attachSession(WebdavRequest request) throws DavException {
|
||||
// every user gets a session
|
||||
request.setDavSession(new WebDavSession());
|
||||
// every request gets a session
|
||||
request.setDavSession(new WebDavSession(request));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class AbstractEncryptedNode implements DavResource {
|
||||
abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
|
||||
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
|
||||
@@ -72,7 +72,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
} catch (IOException e) {
|
||||
@@ -173,8 +173,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public void move(DavResource dest) throws DavException {
|
||||
final Path src = PathUtils.getPhysicalPath(this);
|
||||
final Path dst = PathUtils.getPhysicalPath(dest);
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
@@ -195,8 +195,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
final Path src = PathUtils.getPhysicalPath(this);
|
||||
final Path dst = PathUtils.getPhysicalPath(dest);
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
|
||||
@@ -64,7 +64,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = PathUtils.getPhysicalPath(resource);
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try {
|
||||
Files.createDirectories(childPath);
|
||||
} catch (SecurityException e) {
|
||||
@@ -76,7 +76,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = PathUtils.getPhysicalPath(resource);
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
|
||||
@@ -94,7 +94,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
final Path dir = PathUtils.getPhysicalPath(this);
|
||||
final Path dir = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
@@ -116,7 +116,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
final Path memberPath = PathUtils.getPhysicalPath(member);
|
||||
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
|
||||
try {
|
||||
Files.walkFileTree(memberPath, new DeletingFileVisitor());
|
||||
} catch (SecurityException e) {
|
||||
@@ -133,14 +133,14 @@ public class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
// don't add any further properties
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -63,9 +65,10 @@ public class EncryptedFile extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(path, StandardOpenOption.READ);
|
||||
@@ -81,13 +84,12 @@ public class EncryptedFile extends AbstractEncryptedNode {
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
@@ -96,8 +98,9 @@ public class EncryptedFile extends AbstractEncryptedNode {
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
|
||||
*/
|
||||
public class EncryptedFilePart extends EncryptedFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
||||
private static final String BYTE_UNIT_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
|
||||
/**
|
||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
||||
|
||||
/**
|
||||
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
|
||||
|
||||
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (rangeHeader == null) {
|
||||
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
|
||||
}
|
||||
determineByteRanges(rangeHeader);
|
||||
}
|
||||
|
||||
private void determineByteRanges(String rangeHeader) {
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length == 0) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
for (final String byteRange : byteRanges) {
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
|
||||
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
|
||||
if (lower > upper) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> range : requestedContentRanges) {
|
||||
final long left;
|
||||
final long right;
|
||||
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
|
||||
left = lastByte - range.getRight();
|
||||
right = lastByte;
|
||||
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
|
||||
left = range.getLeft();
|
||||
right = lastByte;
|
||||
} else {
|
||||
left = range.getLeft();
|
||||
right = range.getRight();
|
||||
}
|
||||
if (result.getLeft() == null || left < result.getLeft()) {
|
||||
result.setLeft(left);
|
||||
}
|
||||
if (result.getRight() == null || right > result.getRight()) {
|
||||
result.setRight(right);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
SeekableByteChannel channel = null;
|
||||
try {
|
||||
channel = Files.newByteChannel(path, StandardOpenOption.READ);
|
||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading file " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.Temporal;
|
||||
|
||||
final class FileTimeUtils {
|
||||
|
||||
private FileTimeUtils() {
|
||||
throw new IllegalStateException("not instantiable");
|
||||
}
|
||||
|
||||
static String toRfc1123String(FileTime time) {
|
||||
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
|
||||
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
|
||||
class HttpHeaderProperty extends AbstractDavProperty<String> {
|
||||
|
||||
private final String value;
|
||||
|
||||
public HttpHeaderProperty(String key, String value) {
|
||||
super(DavPropertyName.create(key), true);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import java.nio.file.Path;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
|
||||
public final class PathUtils {
|
||||
public final class ResourcePathUtils {
|
||||
|
||||
private PathUtils() {
|
||||
private ResourcePathUtils() {
|
||||
throw new IllegalStateException("not instantiable");
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
|
||||
|
||||
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
|
||||
|
||||
<appender name="console" class="org.apache.log4j.ConsoleAppender">
|
||||
<param name="Target" value="System.out"/>
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
</layout>
|
||||
<filter class="org.apache.log4j.varia.LevelRangeFilter">
|
||||
<param name="LevelMin" value="debug" />
|
||||
<param name="LevelMax" value="info" />
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="stderr" class="org.apache.log4j.ConsoleAppender">
|
||||
<param name="Target" value="System.err"/>
|
||||
<param name="threshold" value="warn" />
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<appender name="fileAppender" class="org.apache.log4j.DailyRollingFileAppender">
|
||||
<param name="File" value="/tmp/webdav.log" />
|
||||
<param name="Append" value="true" />
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root>
|
||||
<priority value="DEBUG" />
|
||||
<appender-ref ref="console" />
|
||||
<appender-ref ref="stderr" />
|
||||
</root>
|
||||
|
||||
|
||||
</log4j:configuration>
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
@@ -24,12 +24,6 @@
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
@@ -48,33 +42,10 @@
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -26,9 +26,13 @@ import java.security.spec.KeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
@@ -42,12 +46,14 @@ import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.AbstractCryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
|
||||
@@ -84,8 +90,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
*/
|
||||
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
|
||||
|
||||
private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
|
||||
private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
|
||||
private static final int SIZE_OF_LONG = Long.BYTES;
|
||||
|
||||
static {
|
||||
try {
|
||||
@@ -99,16 +104,27 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the masterkey with new random bytes.
|
||||
* Creates a new Cryptor with a newly initialized PRNG.
|
||||
*/
|
||||
public void randomizeMasterKey() {
|
||||
public Aes256Cryptor() {
|
||||
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
|
||||
SECURE_PRNG.nextBytes(this.masterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Cryptor with the given PRNG.<br/>
|
||||
* <strong>DO NOT USE IN PRODUCTION</strong>. This constructor must only be used in in unit tests. Do not change method visibility.
|
||||
*
|
||||
* @param prng Fast, possibly insecure PRNG.
|
||||
*/
|
||||
Aes256Cryptor(Random prng) {
|
||||
prng.nextBytes(this.masterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
|
||||
*/
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
try {
|
||||
// derive key:
|
||||
@@ -144,6 +160,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
*/
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
byte[] decrypted = new byte[0];
|
||||
try {
|
||||
@@ -245,76 +262,112 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
}
|
||||
|
||||
private long crc32Sum(byte[] source) {
|
||||
final CRC32 crc32 = new CRC32();
|
||||
crc32.update(source);
|
||||
return crc32.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep) {
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
try {
|
||||
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
|
||||
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
|
||||
for (final String cleartext : cleartextPathComps) {
|
||||
final String encrypted = encryptPathComponent(cleartext, key);
|
||||
final String encrypted = encryptPathComponent(cleartext, key, ioSupport);
|
||||
encryptedPathComps.add(encrypted);
|
||||
}
|
||||
return StringUtils.join(encryptedPathComps, encryptedPathSep);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
|
||||
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
if (cleartext.length() > PLAINTEXT_FILENAME_LENGTH_LIMIT) {
|
||||
return encryptLongPathComponent(cleartext, key);
|
||||
/**
|
||||
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
|
||||
* Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file
|
||||
* systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in
|
||||
* {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
|
||||
* cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
|
||||
* with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
|
||||
* alternative names are stored.<br/>
|
||||
* <br/>
|
||||
* These alternative names consist of the checksum, a unique id and a special file extension defined in
|
||||
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
|
||||
*/
|
||||
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
|
||||
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
|
||||
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
|
||||
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
|
||||
final String encrypted = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
|
||||
|
||||
if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String crc32 = Long.toHexString(crc32Sum(encrypted.getBytes()));
|
||||
final String metadataFilename = crc32 + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(encrypted).toString() + LONG_NAME_FILE_EXT;
|
||||
this.storeMetadata(ioSupport, metadataFilename, metadata);
|
||||
return alternativeFileName;
|
||||
} else {
|
||||
return encryptShortPathComponent(cleartext, key);
|
||||
return encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptShortPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
|
||||
final byte[] encryptedBytes = cipher.doFinal(cleartext.getBytes(Charsets.UTF_8));
|
||||
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
|
||||
}
|
||||
|
||||
private String encryptLongPathComponent(String cleartext, SecretKey key) {
|
||||
throw new UnsupportedOperationException("not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep) {
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
try {
|
||||
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
|
||||
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
|
||||
for (final String encrypted : encryptedPathComps) {
|
||||
final String cleartext = decryptPathComponent(encrypted, key);
|
||||
final String cleartext = decryptPathComponent(encrypted, key, ioSupport);
|
||||
cleartextPathComps.add(new String(cleartext));
|
||||
}
|
||||
return StringUtils.join(cleartextPathComps, cleartextPathSep);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
|
||||
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
/**
|
||||
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
|
||||
*/
|
||||
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
|
||||
final String ciphertext;
|
||||
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
|
||||
return decryptLongPathComponent(encrypted, key);
|
||||
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
|
||||
final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
|
||||
final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
|
||||
final String metadataFilename = crc32 + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
|
||||
return decryptShortPathComponent(encrypted, key);
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptShortPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
|
||||
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE);
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(basename);
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
|
||||
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
|
||||
return new String(cleartextBytes, Charsets.UTF_8);
|
||||
}
|
||||
|
||||
private String decryptLongPathComponent(final String encrypted, final SecretKey key) {
|
||||
throw new UnsupportedOperationException("not yet implemented");
|
||||
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
|
||||
final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
|
||||
if (fileContent == null) {
|
||||
return new LongFilenameMetadata();
|
||||
} else {
|
||||
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
|
||||
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,8 +399,39 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher);
|
||||
return IOUtils.copyLarge(in, cipheredOut);
|
||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
||||
return IOUtils.copyLarge(cipheredIn, plaintextFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
|
||||
// skip content size:
|
||||
encryptedFile.position(SIZE_OF_LONG);
|
||||
|
||||
// read iv:
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int read = encryptedFile.read(countingIv);
|
||||
if (read != AES_BLOCK_LENGTH) {
|
||||
throw new IOException("Failed to read encrypted file header.");
|
||||
}
|
||||
|
||||
// seek relevant position and update iv:
|
||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
|
||||
|
||||
// fast forward stream:
|
||||
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
|
||||
|
||||
// derive secret key and generate cipher:
|
||||
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
||||
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,18 +439,22 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
// truncate file
|
||||
encryptedFile.truncate(0);
|
||||
|
||||
// use an IV, whose last 4 bytes store an integer used in counter mode and write initial value to file.
|
||||
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
|
||||
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - SIZE_OF_INT, 0);
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
|
||||
countingIv.position(0);
|
||||
|
||||
// derive secret key and generate cipher:
|
||||
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE);
|
||||
|
||||
// skip 8 bytes (reserved for file size):
|
||||
encryptedFile.position(SIZE_OF_LONG);
|
||||
// 8 bytes (file size: temporarily -1):
|
||||
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
|
||||
fileSize.putLong(-1L);
|
||||
fileSize.position(0);
|
||||
encryptedFile.write(fileSize);
|
||||
|
||||
// write iv:
|
||||
// 16 bytes (iv):
|
||||
encryptedFile.write(countingIv);
|
||||
|
||||
// write content:
|
||||
@@ -375,11 +463,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
|
||||
|
||||
// write filesize
|
||||
final ByteBuffer actualSizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
|
||||
actualSizeBuffer.putLong(actualSize);
|
||||
actualSizeBuffer.position(0);
|
||||
fileSize.position(0);
|
||||
fileSize.putLong(actualSize);
|
||||
fileSize.position(0);
|
||||
encryptedFile.position(0);
|
||||
encryptedFile.write(actualSizeBuffer);
|
||||
encryptedFile.write(fileSize);
|
||||
|
||||
return actualSize;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@ interface AesCryptographicConfiguration {
|
||||
int PRNG_SEED_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Number of bytes of the master key. Should be significantly higher than the {@link #AES_KEY_LENGTH}, as a corrupted masterkey can't be
|
||||
* changed without decrypting and re-encrypting all files first.
|
||||
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
|
||||
*/
|
||||
int MASTER_KEY_LENGTH = 512;
|
||||
int MASTER_KEY_LENGTH = 256;
|
||||
|
||||
/**
|
||||
* Number of bytes used as salt, where needed.
|
||||
|
||||
@@ -28,23 +28,31 @@ interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
|
||||
* 144 and 160 are multiples of 16 (128bit aes block size).<br/>
|
||||
* 144 * 8/5 (base32) = 230,..<br/>
|
||||
* 160 * 8/5 = 256<br/>
|
||||
* Base 64 isn't supported on case-insensitive file systems.<br/>
|
||||
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int PLAINTEXT_FILENAME_LENGTH_LIMIT = 144;
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
|
||||
|
||||
/**
|
||||
* For plaintext file names <= {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
|
||||
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String BASIC_FILE_EXT = ".aes";
|
||||
|
||||
/**
|
||||
* For plaintext file names > {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
|
||||
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_NAME_FILE_EXT = ".lng.aes";
|
||||
|
||||
/**
|
||||
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
*/
|
||||
String LONG_NAME_PREFIX_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
|
||||
* kind of uniform distribution for better load balancing.
|
||||
*/
|
||||
String METADATA_FILE_EXT = ".meta";
|
||||
|
||||
/**
|
||||
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
class LongFilenameMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
|
||||
return encryptedFilenames.get(uuid);
|
||||
}
|
||||
|
||||
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
|
||||
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID();
|
||||
encryptedFilenames.put(uuid, encryptedFilename);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
@JsonPropertyOrder(value = { "iv", "salt", "files" })
|
||||
class Metadata implements Serializable {
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
private byte[] iv;
|
||||
private byte[] salt;
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<String, byte[]> filenames;
|
||||
private Map<String, Long> filesizes;
|
||||
|
||||
Metadata() {
|
||||
// used by jackson
|
||||
}
|
||||
|
||||
Metadata(byte[] iv, byte[] salt) {
|
||||
this.iv = iv;
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public void setIv(byte[] iv) {
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public BidiMap<String, byte[]> getFilenames() {
|
||||
if (filenames == null) {
|
||||
filenames = new DualHashBidiMap<>();
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
|
||||
public void setFilenames(BidiMap<String, byte[]> filesnames) {
|
||||
this.filenames = filesnames;
|
||||
}
|
||||
|
||||
public Map<String, Long> getFilesizes() {
|
||||
if (filesizes == null) {
|
||||
filesizes = new HashMap<>();
|
||||
}
|
||||
return filesizes;
|
||||
}
|
||||
|
||||
public void setFilesizes(Map<String, Long> filesizes) {
|
||||
this.filesizes = filesizes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,97 +8,158 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class Aes256CryptorTest {
|
||||
|
||||
private Path tmpDir;
|
||||
private Path masterKey;
|
||||
|
||||
@Before
|
||||
public void prepareTmpDir() throws IOException {
|
||||
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
|
||||
final Path path = FileSystems.getDefault().getPath(tmpDirName);
|
||||
tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
|
||||
masterKey = tmpDir.resolve("test" + Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
}
|
||||
|
||||
@After
|
||||
public void dropTmpDir() throws IOException {
|
||||
FileUtils.deleteDirectory(tmpDir.toFile());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private static final Random TEST_PRNG = new Random();
|
||||
|
||||
@Test
|
||||
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final InputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
decryptor.decryptMasterKey(in, pw);
|
||||
|
||||
IOUtils.closeQuietly(out);
|
||||
IOUtils.closeQuietly(in);
|
||||
}
|
||||
|
||||
@Test(expected = WrongPasswordException.class)
|
||||
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final String wrongPw = "foo";
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
final InputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
decryptor.decryptMasterKey(in, wrongPw);
|
||||
|
||||
IOUtils.closeQuietly(out);
|
||||
IOUtils.closeQuietly(in);
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchFileException.class)
|
||||
public void testWrongLocation() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
@Test
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json");
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ);
|
||||
decryptor.decryptMasterKey(in, pw);
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
IOUtils.closeQuietly(encryptedOut);
|
||||
|
||||
// decrypt:
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertTrue(numDecryptedBytes > 0);
|
||||
|
||||
// check decrypted data:
|
||||
final byte[] result = plaintextOut.toByteArray();
|
||||
Assert.assertArrayEquals(plaintextData, result);
|
||||
}
|
||||
|
||||
@Test(expected = FileAlreadyExistsException.class)
|
||||
public void testReInitialization() throws IOException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
@Test
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 65536; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
IOUtils.closeQuietly(encryptedOut);
|
||||
|
||||
// decrypt:
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertTrue(numDecryptedBytes > 0);
|
||||
|
||||
// check decrypted data:
|
||||
final byte[] result = plaintextOut.toByteArray();
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfFilenames() throws IOException {
|
||||
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
|
||||
|
||||
// short path components
|
||||
final String originalPath1 = "foo/bar/baz";
|
||||
final String encryptedPath1 = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath1, decryptedPath1);
|
||||
|
||||
// long path components
|
||||
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||||
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
|
||||
final String encryptedPath2 = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath2, decryptedPath2);
|
||||
}
|
||||
|
||||
private static class CryptoIOSupportMock implements CryptorIOSupport {
|
||||
|
||||
private final Map<String, byte[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
|
||||
map.put(encryptedPath, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) {
|
||||
return map.get(encryptedPath);
|
||||
}
|
||||
|
||||
final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.encryptMasterKey(outAgain, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
class ByteBufferBackedSeekableChannel implements SeekableByteChannel {
|
||||
|
||||
private final ByteBuffer buffer;
|
||||
private boolean open = true;
|
||||
|
||||
ByteBufferBackedSeekableChannel(ByteBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
open = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst) throws IOException {
|
||||
if (buffer.remaining() == 0) {
|
||||
return -1;
|
||||
}
|
||||
int num = Math.min(dst.remaining(), buffer.remaining());
|
||||
byte[] bytes = new byte[num];
|
||||
buffer.get(bytes);
|
||||
dst.put(bytes);
|
||||
return num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws IOException {
|
||||
int num = src.remaining();
|
||||
if (buffer.remaining() < src.remaining()) {
|
||||
buffer.limit(buffer.limit() + src.remaining());
|
||||
}
|
||||
buffer.put(src);
|
||||
return num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws IOException {
|
||||
return buffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
if (newPosition > Integer.MAX_VALUE) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (newPosition > buffer.limit()) {
|
||||
buffer.limit((int) newPosition);
|
||||
}
|
||||
buffer.position((int) newPosition);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return buffer.limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws IOException {
|
||||
if (size > Integer.MAX_VALUE) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
buffer.limit((int) size);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
<name>Cryptomator cryptographic module API</name>
|
||||
@@ -22,21 +22,9 @@
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,3 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
@@ -15,11 +15,31 @@ import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
/**
|
||||
* Provides access to cryptographic functions. All methods are threadsafe.
|
||||
*/
|
||||
public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
|
||||
/**
|
||||
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
|
||||
*/
|
||||
void encryptMasterKey(OutputStream out, CharSequence password) throws IOException;
|
||||
|
||||
/**
|
||||
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
|
||||
*
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
|
||||
* password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
*/
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
|
||||
|
||||
/**
|
||||
* Encrypts each plaintext path component for its own.
|
||||
*
|
||||
@@ -32,7 +52,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
|
||||
* encrypted path is explicitly absolute.
|
||||
*/
|
||||
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep);
|
||||
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
|
||||
/**
|
||||
* Decrypts each encrypted path component for its own.
|
||||
@@ -46,7 +66,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
|
||||
* cleartext path is explicitly absolute.
|
||||
*/
|
||||
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep);
|
||||
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
|
||||
/**
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
@@ -59,6 +79,13 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
*/
|
||||
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @param pos First byte (inclusive)
|
||||
* @param length Number of requested bytes beginning at pos.
|
||||
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
|
||||
*/
|
||||
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
/**
|
||||
* Optional monitoring interface. If a cryptor implements this interface, it counts bytes de- and encrypted in a thread-safe manner.
|
||||
*/
|
||||
public interface CryptorIOSampling {
|
||||
|
||||
/**
|
||||
* @return Number of encrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollEncryptedBytes(boolean resetCounter);
|
||||
|
||||
/**
|
||||
* @return Number of decrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollDecryptedBytes(boolean resetCounter);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Methods that may be called by the Cryptor when accessing a path.
|
||||
*/
|
||||
public interface CryptorIOSupport {
|
||||
|
||||
/**
|
||||
* Persists encryptedMetadata to the given encryptedPath.
|
||||
*
|
||||
* @param encryptedPath A relative path
|
||||
* @throws IOException
|
||||
*/
|
||||
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
|
||||
*/
|
||||
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
|
||||
private SamplingDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return encryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return decryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
cryptor.encryptMasterKey(out, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
|
||||
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
|
||||
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptedFile(encryptedFile, countingInputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.addSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.removeSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingInputStream(AtomicLong counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.addAndGet(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = in.read(b, off, len);
|
||||
counter.addAndGet(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingOutputStream(AtomicLong counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.incrementAndGet();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.addAndGet(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
public interface SensitiveDataSwipeListener {
|
||||
|
||||
75
main/pom.xml
75
main/pom.xml
@@ -1,25 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<!-- 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 -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.3.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
|
||||
<organization>
|
||||
<name>cryptomator.org</name>
|
||||
<url>http://cryptomator.org</url>
|
||||
</organization>
|
||||
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Sebastian Stenzel</name>
|
||||
@@ -32,8 +25,8 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- dependency versions -->
|
||||
<log4j.version>1.2.16</log4j.version>
|
||||
<slf4j.version>1.7.5</slf4j.version>
|
||||
<log4j.version>2.1</log4j.version>
|
||||
<slf4j.version>1.7.7</slf4j.version>
|
||||
<junit.version>4.11</junit.version>
|
||||
<commons-io.version>2.4</commons-io.version>
|
||||
<commons-collections.version>4.0</commons-collections.version>
|
||||
@@ -64,22 +57,27 @@
|
||||
<artifactId>ui</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-jul</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- commons -->
|
||||
@@ -121,6 +119,25 @@
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-jul</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<modules>
|
||||
<module>crypto-api</module>
|
||||
<module>crypto-aes</module>
|
||||
@@ -128,4 +145,18 @@
|
||||
<module>ui</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
BIN
main/ui/package/linux/Cryptomator.png
Normal file
BIN
main/ui/package/linux/Cryptomator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
main/ui/package/macosx/Cryptomator-background.png
Normal file
BIN
main/ui/package/macosx/Cryptomator-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
main/ui/package/macosx/Cryptomator.icns
Normal file
BIN
main/ui/package/macosx/Cryptomator.icns
Normal file
Binary file not shown.
BIN
main/ui/package/windows/Cryptomator.ico
Normal file
BIN
main/ui/package/windows/Cryptomator.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
@@ -21,6 +21,7 @@
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
<controlsfx.version>8.20.8</controlsfx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -48,84 +49,72 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- UI -->
|
||||
<dependency>
|
||||
<groupId>org.controlsfx</groupId>
|
||||
<artifactId>controlsfx</artifactId>
|
||||
<version>${controlsfx.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy</id>
|
||||
<phase>prepare-package</phase>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeScope>compile</includeScope>
|
||||
<includeScope>runtime</includeScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<finalName>${javafx.application.name}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>${exec.mainClass}</Main-Class>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>native-launcher</id>
|
||||
<phase>package</phase>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${javafx.tools.ant.jar}" />
|
||||
<fx:application id="fxApp" version="${project.version}" name="${javafx.application.name}" mainClass="${exec.mainClass}" />
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<fx:jar destfile="${project.build.directory}/${project.build.finalName}">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}/classes" />
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/*.jar" />
|
||||
</fx:resources>
|
||||
</fx:jar>
|
||||
|
||||
<fx:deploy outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" nativeBundles="all">
|
||||
<fx:info title="Cryptomator" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT">
|
||||
<!-- todo provide .ico files for win -->
|
||||
<fx:icon href="${project.build.outputDirectory}/logo.icns" width="512" height="512" />
|
||||
</fx:info>
|
||||
<fx:deploy nativeBundles="all" outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" verbose="false">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform basedir="" javafx="2.2+" j2se="8.0" />
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:resources>
|
||||
<!-- If you changed <fx:jar> above, don't forget to modify the line below -->
|
||||
<fx:fileset dir="${project.build.directory}" includes="${project.build.finalName}.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/*.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
|
||||
</fx:resources>
|
||||
<fx:preferences install="false" />
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.util.MasterKeyFilter;
|
||||
import org.cryptomator.ui.util.WebDavMounter;
|
||||
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
|
||||
import org.cryptomator.webdav.WebDAVServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AccessController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);
|
||||
|
||||
private final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
private ResourceBundle localization;
|
||||
@FXML
|
||||
private GridPane rootGridPane;
|
||||
@FXML
|
||||
private TextField workDirTextField;
|
||||
@FXML
|
||||
private ComboBox<String> usernameBox;
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
@FXML
|
||||
private Button startServerButton;
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
|
||||
usernameBox.valueProperty().addListener(new UsernameChangeListener());
|
||||
workDirTextField.setText(Settings.load().getWebdavWorkDir());
|
||||
usernameBox.setValue(Settings.load().getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: Choose encrypted storage:
|
||||
*/
|
||||
@FXML
|
||||
protected void chooseWorkDir(ActionEvent event) {
|
||||
messageLabel.setText(null);
|
||||
final File currentFolder = new File(workDirTextField.getText());
|
||||
final DirectoryChooser dirChooser = new DirectoryChooser();
|
||||
if (currentFolder.exists()) {
|
||||
dirChooser.setInitialDirectory(currentFolder);
|
||||
}
|
||||
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
|
||||
if (file != null) {
|
||||
workDirTextField.setText(file.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private final class WorkDirChangeListener implements ChangeListener<String> {
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.isEmpty(newValue)) {
|
||||
usernameBox.setDisable(true);
|
||||
usernameBox.setValue(null);
|
||||
return;
|
||||
}
|
||||
boolean storageLocationValid;
|
||||
try {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(storagePath);
|
||||
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
|
||||
usernameBox.getItems().clear();
|
||||
for (final Path path : ds) {
|
||||
final String fileName = path.getFileName().toString();
|
||||
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
|
||||
final String baseName = fileName.substring(0, beginOfExt);
|
||||
usernameBox.getItems().add(baseName);
|
||||
}
|
||||
storageLocationValid = !usernameBox.getItems().isEmpty();
|
||||
} catch (InvalidPathException | IOException ex) {
|
||||
LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
|
||||
storageLocationValid = false;
|
||||
}
|
||||
// valid encrypted folder?
|
||||
if (storageLocationValid) {
|
||||
Settings.load().setWebdavWorkDir(workDirTextField.getText());
|
||||
Settings.save();
|
||||
} else {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
|
||||
}
|
||||
// enable/disable next controls:
|
||||
usernameBox.setDisable(!storageLocationValid);
|
||||
if (usernameBox.getItems().size() == 1) {
|
||||
usernameBox.setValue(usernameBox.getItems().get(0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Choose username
|
||||
*/
|
||||
private final class UsernameChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (newValue != null) {
|
||||
Settings.load().setUsername(newValue);
|
||||
Settings.save();
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
startServerButton.setDisable(StringUtils.isEmpty(newValue));
|
||||
Platform.runLater(passwordField::requestFocus);
|
||||
}
|
||||
}
|
||||
|
||||
// step 3: Enter password
|
||||
|
||||
/**
|
||||
* Step 4: Unlock storage
|
||||
*/
|
||||
@FXML
|
||||
protected void startStopServer(ActionEvent event) {
|
||||
messageLabel.setText(null);
|
||||
if (WebDAVServer.getInstance().isRunning()) {
|
||||
this.tryStop();
|
||||
cryptor.swipeSensitiveData();
|
||||
} else if (this.unlockStorage()) {
|
||||
this.tryStart();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean unlockStorage() {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
|
||||
final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
InputStream masterKeyInputStream = null;
|
||||
try {
|
||||
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
|
||||
cryptor.decryptMasterKey(masterKeyInputStream, password);
|
||||
return true;
|
||||
} catch (NoSuchFileException e) {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
|
||||
LOG.warn("Invalid path: " + storagePath.toString());
|
||||
} catch (DecryptFailedException ex) {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
} catch (WrongPasswordException e) {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.wrongPassword"));
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.unsupportedKeyLengthInstallJCE"));
|
||||
LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("I/O Exception", ex);
|
||||
} finally {
|
||||
passwordField.swipe();
|
||||
IOUtils.closeQuietly(masterKeyInputStream);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void tryStart() {
|
||||
final Settings settings = Settings.load();
|
||||
final int webdavPort = WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), cryptor);
|
||||
if (webdavPort > 0) {
|
||||
startServerButton.setText(localization.getString("access.button.stopServer"));
|
||||
passwordField.setDisable(true);
|
||||
try {
|
||||
WebDavMounter.mount(webdavPort);
|
||||
} catch (CommandFailedException e) {
|
||||
messageLabel.setText(String.format(localization.getString("access.messageLabel.mountFailed"), webdavPort));
|
||||
LOG.error("Mounting WebDAV share failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryStop() {
|
||||
try {
|
||||
WebDavMounter.unmount(5);
|
||||
if (WebDAVServer.getInstance().stop()) {
|
||||
startServerButton.setText(localization.getString("access.button.startServer"));
|
||||
passwordField.setDisable(false);
|
||||
}
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("Unmounting WebDAV share failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,193 +8,231 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.files.EncryptingFileVisitor;
|
||||
import org.cryptomator.ui.controls.ClearOnDisableListener;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.util.MasterKeyFilter;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
import org.cryptomator.ui.util.FXThreads;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InitializeController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
|
||||
private static final int MAX_USERNAME_LENGTH = 200;
|
||||
private static final int MAX_USERNAME_LENGTH = 250;
|
||||
|
||||
private ResourceBundle localization;
|
||||
@FXML
|
||||
private GridPane rootGridPane;
|
||||
@FXML
|
||||
private TextField workDirTextField;
|
||||
private Directory directory;
|
||||
private InitializationListener listener;
|
||||
|
||||
@FXML
|
||||
private TextField usernameField;
|
||||
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
|
||||
@FXML
|
||||
private SecPasswordField retypePasswordField;
|
||||
|
||||
@FXML
|
||||
private Button initWorkDirButton;
|
||||
private Button okButton;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator progressIndicator;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
|
||||
usernameField.addEventFilter(KeyEvent.KEY_TYPED, new AlphaNumericKeyTypeEventFilter());
|
||||
usernameField.textProperty().addListener(new UsernameChangeListener());
|
||||
usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField));
|
||||
passwordField.textProperty().addListener(new PasswordChangeListener());
|
||||
passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField));
|
||||
retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener());
|
||||
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
|
||||
usernameField.textProperty().addListener(this::usernameFieldDidChange);
|
||||
passwordField.textProperty().addListener(this::passwordFieldDidChange);
|
||||
retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
|
||||
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: Choose a directory, that shall be encrypted. On success, step 2 will be enabled.
|
||||
*/
|
||||
// ****************************************
|
||||
// Username field
|
||||
// ****************************************
|
||||
|
||||
public void filterAlphanumericKeyEvents(KeyEvent t) {
|
||||
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
|
||||
return;
|
||||
}
|
||||
char c = t.getCharacter().charAt(0);
|
||||
if (!CharUtils.isAsciiAlphanumeric(c)) {
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
|
||||
public void usernameFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
|
||||
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Password field
|
||||
// ****************************************
|
||||
|
||||
private void passwordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
retypePasswordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Retype password field
|
||||
// ****************************************
|
||||
|
||||
private void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
|
||||
okButton.setDisable(!passwordsAreEqual);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// OK button
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
protected void chooseWorkDir(ActionEvent event) {
|
||||
final File currentFolder = new File(workDirTextField.getText());
|
||||
final DirectoryChooser dirChooser = new DirectoryChooser();
|
||||
if (currentFolder.exists()) {
|
||||
dirChooser.setInitialDirectory(currentFolder);
|
||||
protected void initializeVault(ActionEvent event) {
|
||||
setControlsDisabled(true);
|
||||
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
|
||||
return;
|
||||
}
|
||||
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
|
||||
if (file != null && file.canWrite()) {
|
||||
workDirTextField.setText(file.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private final class WorkDirChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.isEmpty(newValue)) {
|
||||
usernameField.setDisable(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Path dir = FileSystems.getDefault().getPath(newValue);
|
||||
final boolean containsMasterKeys = MasterKeyFilter.filteredDirectory(dir).iterator().hasNext();
|
||||
if (containsMasterKeys) {
|
||||
usernameField.setDisable(true);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} else {
|
||||
usernameField.setDisable(false);
|
||||
messageLabel.setText(null);
|
||||
}
|
||||
} catch (InvalidPathException | IOException e) {
|
||||
usernameField.setDisable(true);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Choose a valid username
|
||||
*/
|
||||
private static final class AlphaNumericKeyTypeEventFilter implements EventHandler<KeyEvent> {
|
||||
@Override
|
||||
public void handle(KeyEvent t) {
|
||||
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
|
||||
return;
|
||||
}
|
||||
char c = t.getCharacter().charAt(0);
|
||||
if (!CharUtils.isAsciiAlphanumeric(c)) {
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class UsernameChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
|
||||
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(usernameField.getText()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Defina a password. On success, step 3 will be enabled.
|
||||
*/
|
||||
private final class PasswordChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
retypePasswordField.setDisable(newValue.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4: Retype the password. On success, step 4 will be enabled.
|
||||
*/
|
||||
private final class RetypePasswordChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
|
||||
initWorkDirButton.setDisable(!passwordsAreEqual);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 5: Generate master password file in working directory. On success, print success message.
|
||||
*/
|
||||
@FXML
|
||||
protected void initWorkDir(ActionEvent event) {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final Path masterKeyPath = storagePath.resolve(usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
|
||||
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
|
||||
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
OutputStream masterKeyOutputStream = null;
|
||||
try {
|
||||
progressIndicator.setVisible(true);
|
||||
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.encryptMasterKey(masterKeyOutputStream, password);
|
||||
cryptor.swipeSensitiveData();
|
||||
workDirTextField.clear();
|
||||
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
|
||||
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
|
||||
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
|
||||
progressIndicator.setVisible(false);
|
||||
progressIndicator.setVisible(false);
|
||||
directory.getCryptor().swipeSensitiveData();
|
||||
if (listener != null) {
|
||||
listener.didInitialize(this);
|
||||
}
|
||||
});
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} catch (InvalidPathException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
|
||||
} catch (IOException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
LOG.error("I/O Exception", ex);
|
||||
} finally {
|
||||
swipePasswordFields();
|
||||
usernameField.setText(null);
|
||||
passwordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
IOUtils.closeQuietly(masterKeyOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void swipePasswordFields() {
|
||||
passwordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
private void setControlsDisabled(boolean disable) {
|
||||
usernameField.setDisable(disable);
|
||||
passwordField.setDisable(disable);
|
||||
retypePasswordField.setDisable(disable);
|
||||
okButton.setDisable(disable);
|
||||
}
|
||||
|
||||
private boolean isDirectoryEmpty() {
|
||||
try {
|
||||
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
|
||||
return !dirContents.iterator().hasNext();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to analyze directory.", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldEncryptExistingFiles() {
|
||||
final Alert alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
|
||||
|
||||
final Optional<ButtonType> result = alert.showAndWait();
|
||||
return ButtonType.OK.equals(result.get());
|
||||
}
|
||||
|
||||
private void encryptExistingContents() {
|
||||
try {
|
||||
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
|
||||
Files.walkFileTree(directory.getPath(), visitor);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("I/O Exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldEncryptExistingFile(Path path) {
|
||||
final String name = path.getFileName().toString();
|
||||
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Directory getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(Directory directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public InitializationListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public void setListener(InitializationListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
interface InitializationListener {
|
||||
void didInitialize(InitializeController ctrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,50 +10,90 @@ package org.cryptomator.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.util.WebDavMounter;
|
||||
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
|
||||
import org.cryptomator.webdav.WebDAVServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.cryptomator.ui.util.TrayIconUtil;
|
||||
import org.eclipse.jetty.util.ConcurrentHashSet;
|
||||
|
||||
public class MainApplication extends Application {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
|
||||
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
|
||||
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
Application.launch(args);
|
||||
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(final Stage primaryStage) throws IOException {
|
||||
final ResourceBundle localizations = ResourceBundle.getBundle("localization");
|
||||
final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations);
|
||||
chooseNativeStylesheet();
|
||||
final ResourceBundle rb = ResourceBundle.getBundle("localization");
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
|
||||
final Parent root = loader.load();
|
||||
final MainController ctrl = loader.getController();
|
||||
ctrl.setStage(primaryStage);
|
||||
final Scene scene = new Scene(root);
|
||||
primaryStage.setTitle("Cryptomator");
|
||||
primaryStage.setTitle(rb.getString("app.name"));
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.sizeToScene();
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.show();
|
||||
TrayIconUtil.init(primaryStage, rb, () -> {
|
||||
quit();
|
||||
});
|
||||
}
|
||||
|
||||
private void chooseNativeStylesheet() {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void quit() {
|
||||
Platform.runLater(() -> {
|
||||
CLEAN_SHUTDOWN_PERFORMER.run();
|
||||
Settings.save();
|
||||
Platform.exit();
|
||||
System.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
try {
|
||||
WebDavMounter.unmount(5);
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("Unmounting WebDAV share failed.", e);
|
||||
}
|
||||
WebDAVServer.getInstance().stop();
|
||||
public void stop() {
|
||||
CLEAN_SHUTDOWN_PERFORMER.run();
|
||||
Settings.save();
|
||||
super.stop();
|
||||
}
|
||||
|
||||
public static void addShutdownTask(Runnable r) {
|
||||
SHUTDOWN_TASKS.add(r);
|
||||
}
|
||||
|
||||
public static void removeShutdownTask(Runnable r) {
|
||||
SHUTDOWN_TASKS.remove(r);
|
||||
}
|
||||
|
||||
private static class CleanShutdownPerformer extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
SHUTDOWN_TASKS.forEach(r -> {
|
||||
r.run();
|
||||
});
|
||||
SHUTDOWN_TASKS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,48 +8,193 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class MainController {
|
||||
import org.cryptomator.ui.InitializeController.InitializationListener;
|
||||
import org.cryptomator.ui.UnlockController.UnlockListener;
|
||||
import org.cryptomator.ui.UnlockedController.LockListener;
|
||||
import org.cryptomator.ui.controls.DirectoryListCell;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
|
||||
|
||||
private Stage stage;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup toolbarButtonGroup;
|
||||
private ContextMenu directoryContextMenu;
|
||||
|
||||
@FXML
|
||||
private VBox rootVBox;
|
||||
private HBox rootPane;
|
||||
|
||||
@FXML
|
||||
private Pane initializePanel;
|
||||
private ListView<Directory> directoryList;
|
||||
|
||||
@FXML
|
||||
private Pane accessPanel;
|
||||
private Pane contentPane;
|
||||
|
||||
@FXML
|
||||
private Pane advancedPanel;
|
||||
private ResourceBundle rb;
|
||||
|
||||
@FXML
|
||||
protected void showInitializePane(ActionEvent event) {
|
||||
showPanel(initializePanel);
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
|
||||
final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
|
||||
directoryList.setItems(items);
|
||||
directoryList.setCellFactory(this::createDirecoryListCell);
|
||||
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
|
||||
}
|
||||
|
||||
@FXML
|
||||
protected void showAccessPane(ActionEvent event) {
|
||||
showPanel(accessPanel);
|
||||
private void didClickAddDirectory(ActionEvent event) {
|
||||
final DirectoryChooser dirChooser = new DirectoryChooser();
|
||||
final File file = dirChooser.showDialog(stage);
|
||||
if (file != null && file.canWrite()) {
|
||||
final Directory dir = new Directory(file.toPath());
|
||||
directoryList.getItems().add(dir);
|
||||
directoryList.getSelectionModel().selectLast();
|
||||
}
|
||||
}
|
||||
|
||||
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
|
||||
final DirectoryListCell cell = new DirectoryListCell();
|
||||
cell.setContextMenu(directoryContextMenu);
|
||||
return cell;
|
||||
}
|
||||
|
||||
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
|
||||
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
|
||||
if (selectedDir == null) {
|
||||
stage.setTitle(rb.getString("app.name"));
|
||||
showWelcomeView();
|
||||
} else {
|
||||
stage.setTitle(selectedDir.getName());
|
||||
showDirectory(selectedDir);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
protected void showAdvancedPane(ActionEvent event) {
|
||||
showPanel(advancedPanel);
|
||||
private void didClickRemoveSelectedEntry(ActionEvent e) {
|
||||
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
|
||||
directoryList.getItems().remove(selectedDir);
|
||||
directoryList.getSelectionModel().clearSelection();
|
||||
}
|
||||
|
||||
private void showPanel(Pane panel) {
|
||||
rootVBox.getChildren().remove(1);
|
||||
rootVBox.getChildren().add(panel);
|
||||
rootVBox.getScene().getWindow().sizeToScene();
|
||||
// ****************************************
|
||||
// Subcontroller for right panel
|
||||
// ****************************************
|
||||
|
||||
private void showDirectory(Directory directory) {
|
||||
try {
|
||||
if (directory.isUnlocked()) {
|
||||
this.showUnlockedView(directory);
|
||||
} else if (directory.containsMasterKey()) {
|
||||
this.showUnlockView(directory);
|
||||
} else {
|
||||
this.showInitializeView(directory);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to analyze directory.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T showView(String fxml) {
|
||||
try {
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
|
||||
final Parent root = loader.load();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
return loader.getController();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to load fxml file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void showWelcomeView() {
|
||||
this.showView("/fxml/welcome.fxml");
|
||||
}
|
||||
|
||||
private void showInitializeView(Directory directory) {
|
||||
final InitializeController ctrl = showView("/fxml/initialize.fxml");
|
||||
ctrl.setDirectory(directory);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didInitialize(InitializeController ctrl) {
|
||||
showUnlockView(ctrl.getDirectory());
|
||||
}
|
||||
|
||||
private void showUnlockView(Directory directory) {
|
||||
final UnlockController ctrl = showView("/fxml/unlock.fxml");
|
||||
ctrl.setDirectory(directory);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didUnlock(UnlockController ctrl) {
|
||||
showUnlockedView(ctrl.getDirectory());
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
|
||||
private void showUnlockedView(Directory directory) {
|
||||
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
|
||||
ctrl.setDirectory(directory);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didLock(UnlockedController ctrl) {
|
||||
showUnlockView(ctrl.getDirectory());
|
||||
if (getUnlockedDirectories().isEmpty()) {
|
||||
Platform.setImplicitExit(true);
|
||||
}
|
||||
}
|
||||
|
||||
/* Convenience */
|
||||
|
||||
public Collection<Directory> getDirectories() {
|
||||
return directoryList.getItems();
|
||||
}
|
||||
|
||||
public Collection<Directory> getUnlockedDirectories() {
|
||||
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/* public Getter/Setter */
|
||||
|
||||
public Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
189
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java
Normal file
189
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java
Normal file
@@ -0,0 +1,189 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
import org.cryptomator.ui.util.FXThreads;
|
||||
import org.cryptomator.ui.util.MasterKeyFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class UnlockController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
||||
|
||||
private ResourceBundle rb;
|
||||
private UnlockListener listener;
|
||||
private Directory directory;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> usernameBox;
|
||||
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
|
||||
@FXML
|
||||
private Button unlockButton;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator progressIndicator;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
|
||||
usernameBox.valueProperty().addListener(this::didChooseUsername);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Username box
|
||||
// ****************************************
|
||||
|
||||
public void didChooseUsername(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (newValue != null) {
|
||||
Platform.runLater(passwordField::requestFocus);
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Unlock button
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
private void didClickUnlockButton(ActionEvent event) {
|
||||
setControlsDisabled(true);
|
||||
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
|
||||
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
InputStream masterKeyInputStream = null;
|
||||
try {
|
||||
progressIndicator.setVisible(true);
|
||||
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
|
||||
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
|
||||
if (!directory.startServer()) {
|
||||
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
|
||||
directory.getCryptor().swipeSensitiveData();
|
||||
return;
|
||||
}
|
||||
directory.setUnlocked(true);
|
||||
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
|
||||
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
|
||||
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
|
||||
setControlsDisabled(false);
|
||||
});
|
||||
} catch (DecryptFailedException | IOException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
} catch (WrongPasswordException e) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
|
||||
passwordField.requestFocus();
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
|
||||
} finally {
|
||||
passwordField.swipe();
|
||||
IOUtils.closeQuietly(masterKeyInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void setControlsDisabled(boolean disable) {
|
||||
usernameBox.setDisable(disable);
|
||||
passwordField.setDisable(disable);
|
||||
unlockButton.setDisable(disable);
|
||||
}
|
||||
|
||||
private void findExistingUsernames() {
|
||||
try {
|
||||
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
|
||||
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
|
||||
usernameBox.getItems().clear();
|
||||
for (final Path path : ds) {
|
||||
final String fileName = path.getFileName().toString();
|
||||
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
|
||||
final String baseName = fileName.substring(0, beginOfExt);
|
||||
usernameBox.getItems().add(baseName);
|
||||
}
|
||||
if (usernameBox.getItems().size() == 1) {
|
||||
usernameBox.getSelectionModel().selectFirst();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.trace("Invalid path: " + directory.getPath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void didUnlockAndMount(boolean mountSuccess) {
|
||||
progressIndicator.setVisible(false);
|
||||
if (listener != null) {
|
||||
listener.didUnlock(this);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Directory getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(Directory directory) {
|
||||
this.directory = directory;
|
||||
this.findExistingUsernames();
|
||||
}
|
||||
|
||||
public UnlockListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public void setListener(UnlockListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
interface UnlockListener {
|
||||
void didUnlock(UnlockController ctrl);
|
||||
}
|
||||
|
||||
}
|
||||
150
main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java
Normal file
150
main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java
Normal file
@@ -0,0 +1,150 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart.Data;
|
||||
import javafx.scene.chart.XYChart.Series;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import org.cryptomator.crypto.CryptorIOSampling;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
|
||||
public class UnlockedController implements Initializable {
|
||||
|
||||
private static final int IO_SAMPLING_STEPS = 100;
|
||||
private static final double IO_SAMPLING_INTERVAL = 0.25;
|
||||
private ResourceBundle rb;
|
||||
private LockListener listener;
|
||||
private Directory directory;
|
||||
private Timeline ioAnimation;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@FXML
|
||||
private LineChart<Number, Number> ioGraph;
|
||||
|
||||
@FXML
|
||||
private NumberAxis xAxis;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickCloseVault(ActionEvent event) {
|
||||
directory.unmount();
|
||||
directory.stopServer();
|
||||
directory.setUnlocked(false);
|
||||
if (listener != null) {
|
||||
listener.didLock(this);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// IO Graph
|
||||
// ****************************************
|
||||
|
||||
private void startIoSampling(final CryptorIOSampling sampler) {
|
||||
final Series<Number, Number> decryptedBytes = new Series<>();
|
||||
decryptedBytes.setName("decrypted");
|
||||
final Series<Number, Number> encryptedBytes = new Series<>();
|
||||
encryptedBytes.setName("encrypted");
|
||||
|
||||
ioGraph.getData().add(decryptedBytes);
|
||||
ioGraph.getData().add(encryptedBytes);
|
||||
|
||||
ioAnimation = new Timeline();
|
||||
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(sampler, decryptedBytes, encryptedBytes)));
|
||||
ioAnimation.setCycleCount(Animation.INDEFINITE);
|
||||
ioAnimation.play();
|
||||
}
|
||||
|
||||
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
|
||||
|
||||
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
|
||||
private final CryptorIOSampling sampler;
|
||||
private final Series<Number, Number> decryptedBytes;
|
||||
private final Series<Number, Number> encryptedBytes;
|
||||
private int step = 0;
|
||||
|
||||
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
|
||||
this.sampler = sampler;
|
||||
this.decryptedBytes = decryptedBytes;
|
||||
this.encryptedBytes = encryptedBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ActionEvent event) {
|
||||
step++;
|
||||
|
||||
final double decryptedMb = sampler.pollDecryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
|
||||
decryptedBytes.getData().add(new Data<Number, Number>(step, decryptedMb));
|
||||
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
|
||||
decryptedBytes.getData().remove(0);
|
||||
}
|
||||
|
||||
final double encrypteddMb = sampler.pollEncryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
|
||||
encryptedBytes.getData().add(new Data<Number, Number>(step, encrypteddMb));
|
||||
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
|
||||
encryptedBytes.getData().remove(0);
|
||||
}
|
||||
|
||||
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
|
||||
xAxis.setUpperBound(step);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Directory getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(Directory directory) {
|
||||
this.directory = directory;
|
||||
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
|
||||
messageLabel.setText(msg);
|
||||
|
||||
if (directory.getCryptor() instanceof CryptorIOSampling) {
|
||||
startIoSampling((CryptorIOSampling) directory.getCryptor());
|
||||
} else {
|
||||
ioGraph.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
public LockListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public void setListener(LockListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/* callback */
|
||||
|
||||
interface LockListener {
|
||||
void didLock(UnlockedController ctrl);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Circle;
|
||||
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
|
||||
public class DirectoryListCell extends ListCell<Directory> implements ChangeListener<Boolean> {
|
||||
|
||||
// fill: #FD4943, stroke: #E1443F
|
||||
private static final Color RED_FILL = Color.rgb(253, 73, 67);
|
||||
private static final Color RED_STROKE = Color.rgb(225, 68, 63);
|
||||
|
||||
// fill: #28CA40, stroke: #30B740
|
||||
private static final Color GREEN_FILL = Color.rgb(40, 202, 64);
|
||||
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
|
||||
|
||||
private final Circle statusIndicator = new Circle(4.5);
|
||||
|
||||
public DirectoryListCell() {
|
||||
setGraphic(statusIndicator);
|
||||
setGraphicTextGap(12.0);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Directory item, boolean empty) {
|
||||
final Directory oldItem = super.getItem();
|
||||
if (oldItem != null) {
|
||||
oldItem.unlockedProperty().removeListener(this);
|
||||
}
|
||||
super.updateItem(item, empty);
|
||||
if (item == null) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
statusIndicator.setVisible(false);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
setTooltip(new Tooltip(item.getPath().toString()));
|
||||
statusIndicator.setVisible(true);
|
||||
item.unlockedProperty().addListener(this);
|
||||
updateStatusIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
updateStatusIndicator();
|
||||
}
|
||||
|
||||
private void updateStatusIndicator() {
|
||||
final Paint fillColor = getItem().isUnlocked() ? GREEN_FILL : RED_FILL;
|
||||
final Paint strokeColor = getItem().isUnlocked() ? GREEN_STROKE : RED_STROKE;
|
||||
statusIndicator.setFill(fillColor);
|
||||
statusIndicator.setStroke(strokeColor);
|
||||
}
|
||||
|
||||
}
|
||||
154
main/ui/src/main/java/org/cryptomator/ui/model/Directory.java
Normal file
154
main/ui/src/main/java/org/cryptomator/ui/model/Directory.java
Normal file
@@ -0,0 +1,154 @@
|
||||
package org.cryptomator.ui.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.SamplingDecorator;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.ui.MainApplication;
|
||||
import org.cryptomator.ui.util.MasterKeyFilter;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
import org.cryptomator.ui.util.mount.WebDavMount;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
@JsonSerialize(using = DirectorySerializer.class)
|
||||
@JsonDeserialize(using = DirectoryDeserializer.class)
|
||||
public class Directory implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 3754487289683599469L;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
|
||||
|
||||
private final WebDavServer server = new WebDavServer();
|
||||
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
|
||||
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
|
||||
private final Path path;
|
||||
// private boolean unlocked;
|
||||
private WebDavMount webDavMount;
|
||||
private final Runnable shutdownTask = new ShutdownTask();
|
||||
|
||||
public Directory(final Path path) {
|
||||
if (!Files.isDirectory(path)) {
|
||||
throw new IllegalArgumentException("Not a directory: " + path);
|
||||
}
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean containsMasterKey() throws IOException {
|
||||
return MasterKeyFilter.filteredDirectory(path).iterator().hasNext();
|
||||
}
|
||||
|
||||
public synchronized boolean startServer() {
|
||||
if (server.start(path.toString(), cryptor)) {
|
||||
MainApplication.addShutdownTask(shutdownTask);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stopServer() {
|
||||
if (server.isRunning()) {
|
||||
MainApplication.removeShutdownTask(shutdownTask);
|
||||
this.unmount();
|
||||
server.stop();
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean mount() {
|
||||
try {
|
||||
webDavMount = WebDavMounter.mount(server.getPort());
|
||||
return true;
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("mount failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unmount() {
|
||||
try {
|
||||
if (webDavMount != null) {
|
||||
webDavMount.unmount();
|
||||
webDavMount = null;
|
||||
}
|
||||
return true;
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("unmount failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directory name without preceeding path components
|
||||
*/
|
||||
public String getName() {
|
||||
return path.getFileName().toString();
|
||||
}
|
||||
|
||||
public Cryptor getCryptor() {
|
||||
return cryptor;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> unlockedProperty() {
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return unlocked.get();
|
||||
}
|
||||
|
||||
public void setUnlocked(boolean unlocked) {
|
||||
this.unlocked.set(unlocked);
|
||||
}
|
||||
|
||||
public WebDavServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
/* hashcode/equals */
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return path.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Directory) {
|
||||
final Directory other = (Directory) obj;
|
||||
return this.path.equals(other.path);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* graceful shutdown */
|
||||
|
||||
private class ShutdownTask implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
stopServer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.ui.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class DirectoryDeserializer extends JsonDeserializer<Directory> {
|
||||
|
||||
@Override
|
||||
public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
final JsonNode node = jp.readValueAsTree();
|
||||
final String pathStr = node.get("path").asText();
|
||||
final Path path = FileSystems.getDefault().getPath(pathStr);
|
||||
return new Directory(path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.cryptomator.ui.model;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
public class DirectorySerializer extends JsonSerializer<Directory> {
|
||||
|
||||
@Override
|
||||
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("path", value.getPath().toString());
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,8 +17,11 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.model.Directory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -40,20 +43,19 @@ public class Settings implements Serializable {
|
||||
final FileSystem fs = FileSystems.getDefault();
|
||||
|
||||
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
|
||||
SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor");
|
||||
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
|
||||
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
|
||||
} else if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/opencloudencryptor");
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
|
||||
} else {
|
||||
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
|
||||
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
|
||||
}
|
||||
}
|
||||
|
||||
private String webdavWorkDir;
|
||||
private List<Directory> directories;
|
||||
private String username;
|
||||
private int port;
|
||||
|
||||
private Settings() {
|
||||
// private constructor
|
||||
@@ -89,19 +91,20 @@ public class Settings implements Serializable {
|
||||
}
|
||||
|
||||
private static Settings defaultSettings() {
|
||||
final Settings result = new Settings();
|
||||
result.setWebdavWorkDir(System.getProperty("user.home", "."));
|
||||
return result;
|
||||
return new Settings();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getWebdavWorkDir() {
|
||||
return webdavWorkDir;
|
||||
public List<Directory> getDirectories() {
|
||||
if (directories == null) {
|
||||
directories = new ArrayList<>();
|
||||
}
|
||||
return directories;
|
||||
}
|
||||
|
||||
public void setWebdavWorkDir(String webdavWorkDir) {
|
||||
this.webdavWorkDir = webdavWorkDir;
|
||||
public void setDirectories(List<Directory> directories) {
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
@@ -112,14 +115,4 @@ public class Settings implements Serializable {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
175
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java
Normal file
175
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java
Normal file
@@ -0,0 +1,175 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* https://github.com/totalvoidness/FXThreads
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.util;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
/**
|
||||
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
|
||||
* <br/>
|
||||
* <strong>Example use (ignoring exceptions):</strong>
|
||||
*
|
||||
* <pre>
|
||||
* // get some string from a remote server:
|
||||
* Future<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* // when done, update text label:
|
||||
* runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* <strong>Example use (exception-aware):</strong>
|
||||
*
|
||||
* <pre>
|
||||
* // get some string from a remote server:
|
||||
* Future<String> futureBookName = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* // when done, update text label:
|
||||
* runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* }, (exception) -> {
|
||||
* myLabel.setText("An exception occured: " + exception.getMessage());
|
||||
* });
|
||||
* </pre>
|
||||
*/
|
||||
public final class FXThreads {
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
|
||||
private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
|
||||
// ignore.
|
||||
};
|
||||
|
||||
private FXThreads() {
|
||||
throw new AssertionError("Not instantiable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
|
||||
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
|
||||
*
|
||||
* <pre>
|
||||
* // examples:
|
||||
*
|
||||
* Future<String> futureBookName1 = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* Future<String> futureBookName2 = runOnBackgroundThread(() -> {
|
||||
* return restResource.getBookName();
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param task The task to be executed on a background thread.
|
||||
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
|
||||
*/
|
||||
public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
|
||||
return EXECUTOR.submit(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
|
||||
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
|
||||
*
|
||||
* <pre>
|
||||
* // examples:
|
||||
*
|
||||
* Future<?> futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
|
||||
*
|
||||
* Future<?> futureDone2 = runOnBackgroundThread(() -> {
|
||||
* doSomeComplexCalculation();
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param task The task to be executed on a background thread.
|
||||
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
|
||||
*/
|
||||
public static Future<?> runOnBackgroundThread(Runnable task) {
|
||||
return EXECUTOR.submit(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
|
||||
* called. If you are interested in the exception, use
|
||||
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||
*
|
||||
* <pre>
|
||||
* // example:
|
||||
*
|
||||
* runOnMainThreadWhenFinished(futureBookName, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param task The task to wait for.
|
||||
* @param successCallback The action to perform, when the task finished.
|
||||
*/
|
||||
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
|
||||
runOnBackgroundThread(() -> {
|
||||
return "asd";
|
||||
});
|
||||
FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
|
||||
* called. If you are interested in the exception, use
|
||||
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||
*
|
||||
* <pre>
|
||||
* // example:
|
||||
*
|
||||
* runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -> {
|
||||
* myLabel.setText(bookName);
|
||||
* }, (exception) -> {
|
||||
* myLabel.setText("An exception occured: " + exception.getMessage());
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param task The task to wait for.
|
||||
* @param successCallback The action to perform, when the task finished.
|
||||
*/
|
||||
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
|
||||
assertParamNotNull(task, "task must not be null.");
|
||||
assertParamNotNull(successCallback, "successCallback must not be null.");
|
||||
assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
|
||||
EXECUTOR.execute(() -> {
|
||||
try {
|
||||
final T result = task.get();
|
||||
Platform.runLater(() -> {
|
||||
successCallback.taskFinished(result);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
exceptionCallback.taskFailed(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertParamNotNull(Object param, String msg) {
|
||||
if (param == null) {
|
||||
throw new IllegalArgumentException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public interface CallbackWhenTaskFinished<T> {
|
||||
void taskFinished(T result);
|
||||
}
|
||||
|
||||
public interface CallbackWhenTaskFailed {
|
||||
void taskFailed(Throwable t);
|
||||
}
|
||||
|
||||
}
|
||||
119
main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java
Normal file
119
main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package org.cryptomator.ui.util;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
public final class TrayIconUtil {
|
||||
|
||||
private static TrayIconUtil INSTANCE;
|
||||
|
||||
private final Stage mainApplicationWindow;
|
||||
private final ResourceBundle rb;
|
||||
private final Runnable exitCommand;
|
||||
|
||||
/**
|
||||
* This will add an icon to the system tray and modify the application shutdown procedure. Depending on
|
||||
* {@link Platform#isImplicitExit()} the application may still be running, allowing shutdown using the tray menu.
|
||||
*/
|
||||
public synchronized static void init(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
|
||||
if (INSTANCE == null && SystemTray.isSupported()) {
|
||||
INSTANCE = new TrayIconUtil(mainApplicationWindow, rb, exitCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private TrayIconUtil(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
|
||||
this.mainApplicationWindow = mainApplicationWindow;
|
||||
this.rb = rb;
|
||||
this.exitCommand = exitCommand;
|
||||
|
||||
initTrayIcon();
|
||||
}
|
||||
|
||||
private void initTrayIcon() {
|
||||
final TrayIcon trayIcon = createTrayIcon();
|
||||
try {
|
||||
SystemTray.getSystemTray().add(trayIcon);
|
||||
mainApplicationWindow.setOnCloseRequest((e) -> {
|
||||
if (Platform.isImplicitExit()) {
|
||||
exitCommand.run();
|
||||
} else {
|
||||
mainApplicationWindow.close();
|
||||
this.showTrayNotification(trayIcon);
|
||||
}
|
||||
});
|
||||
} catch (SecurityException | AWTException ex) {
|
||||
// not working? then just go ahead and close the app
|
||||
mainApplicationWindow.setOnCloseRequest((ev) -> {
|
||||
exitCommand.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private TrayIcon createTrayIcon() {
|
||||
final PopupMenu popup = new PopupMenu();
|
||||
|
||||
final MenuItem showItem = new MenuItem(rb.getString("tray.menu.open"));
|
||||
showItem.addActionListener(this::restoreFromTray);
|
||||
popup.add(showItem);
|
||||
|
||||
final MenuItem exitItem = new MenuItem(rb.getString("tray.menu.quit"));
|
||||
exitItem.addActionListener(this::quitFromTray);
|
||||
popup.add(exitItem);
|
||||
|
||||
final Image image = Toolkit.getDefaultToolkit().getImage(TrayIconUtil.class.getResource("/tray_icon.png"));
|
||||
return new TrayIcon(image, rb.getString("app.name"), popup);
|
||||
}
|
||||
|
||||
private void showTrayNotification(TrayIcon trayIcon) {
|
||||
final Runnable notificationCmd;
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
final String title = rb.getString("tray.infoMsg.title");
|
||||
final String msg = rb.getString("tray.infoMsg.msg.osx");
|
||||
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
|
||||
notificationCmd = () -> {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
|
||||
} catch (IOException e) {
|
||||
// ignore, user will notice the tray icon anyway.
|
||||
}
|
||||
};
|
||||
} else {
|
||||
final String title = rb.getString("tray.infoMsg.title");
|
||||
final String msg = rb.getString("tray.infoMsg.msg");
|
||||
notificationCmd = () -> {
|
||||
trayIcon.displayMessage(title, msg, MessageType.INFO);
|
||||
};
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
notificationCmd.run();
|
||||
});
|
||||
}
|
||||
|
||||
private void restoreFromTray(ActionEvent event) {
|
||||
Platform.runLater(() -> {
|
||||
mainApplicationWindow.show();
|
||||
mainApplicationWindow.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
private void quitFromTray(ActionEvent event) {
|
||||
exitCommand.run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class WebDavMounter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
|
||||
private static final int CMD_DEFAULT_TIMEOUT = 1;
|
||||
|
||||
private WebDavMounter() {
|
||||
throw new IllegalStateException("not instantiable.");
|
||||
}
|
||||
|
||||
public static void mount(int localPort) throws CommandFailedException {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
exec("mkdir /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
|
||||
exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
|
||||
exec("open /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unmount(int timeout) throws CommandFailedException {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
exec("umount /Volumes/Cryptomator", timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private static void exec(String cmd, int timoutSeconds) throws CommandFailedException {
|
||||
try {
|
||||
final Process proc = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
|
||||
if (proc.waitFor(timoutSeconds, TimeUnit.SECONDS)) {
|
||||
proc.destroy();
|
||||
}
|
||||
if (proc.exitValue() != 0) {
|
||||
throw new CommandFailedException(IOUtils.toString(proc.getErrorStream()));
|
||||
}
|
||||
} catch (IOException | InterruptedException | IllegalThreadStateException e) {
|
||||
LOG.error("Command execution failed.", e);
|
||||
throw new CommandFailedException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CommandFailedException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 5784853630182321479L;
|
||||
|
||||
private CommandFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
private CommandFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.command;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.ui.util.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()) {
|
||||
LOG.debug("Command execution finished. Exit code: {}\n" + "Output:\n" + "{}\n" + "Error:\n" + "{}\n", 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.ui.util.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.ui.util.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.ui.util.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.ui.util.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.ui.util.command;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.cryptomator.ui.util.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.ui.util.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,43 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
/**
|
||||
* 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 WebDavMount mount(int localPort) {
|
||||
displayMountInstructions();
|
||||
return new WebDavMount() {
|
||||
@Override
|
||||
public void unmount() {
|
||||
displayUnmountInstructions();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
|
||||
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@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 WebDavMount mount(int localPort) throws CommandFailedException {
|
||||
final Script mountScript = Script.fromLines(
|
||||
"set -x",
|
||||
"gvfs-mount \"dav://[::1]:$PORT\"",
|
||||
"xdg-open \"$URI\"")
|
||||
.addEnv("PORT", String.valueOf(localPort));
|
||||
final Script unmountScript = Script.fromLines(
|
||||
"set -x",
|
||||
"gvfs-mount -u \"dav://[::1]:$PORT\"")
|
||||
.addEnv("URI", String.valueOf(localPort));
|
||||
mountScript.execute();
|
||||
return new WebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
unmountScript.execute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
|
||||
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
return SystemUtils.IS_OS_MAC_OSX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(int localPort) throws CommandFailedException {
|
||||
final String path = "/Volumes/Cryptomator" + localPort;
|
||||
final Script mountScript = Script.fromLines(
|
||||
"mkdir \"$MOUNT_PATH\"",
|
||||
"mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"",
|
||||
"open \"$MOUNT_PATH\"")
|
||||
.addEnv("PORT", String.valueOf(localPort))
|
||||
.addEnv("MOUNT_PATH", path);
|
||||
final Script unmountScript = Script.fromLines(
|
||||
"umount $MOUNT_PATH")
|
||||
.addEnv("MOUNT_PATH", path);
|
||||
mountScript.execute();
|
||||
return new WebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
unmountScript.execute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
|
||||
/**
|
||||
* A mounted webdav share.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public interface WebDavMount {
|
||||
|
||||
/**
|
||||
* Unmounts this {@code WebDavMount}.
|
||||
*
|
||||
* @throws CommandFailedException if the unmount operation fails
|
||||
*/
|
||||
void unmount() throws CommandFailedException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class WebDavMounter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
|
||||
|
||||
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
|
||||
|
||||
private static volatile WebDavMounterStrategy choosenStrategy;
|
||||
|
||||
/**
|
||||
* Tries to mount a given webdav share.
|
||||
*
|
||||
* @param localPort local TCP port of the webdav share
|
||||
* @return a {@link WebDavMount} representing the mounted share
|
||||
* @throws CommandFailedException if the mount operation fails
|
||||
*/
|
||||
public static WebDavMount mount(int localPort) throws CommandFailedException {
|
||||
return chooseStrategy().mount(localPort);
|
||||
}
|
||||
|
||||
private static WebDavMounterStrategy chooseStrategy() {
|
||||
if (choosenStrategy == null) {
|
||||
choosenStrategy = getStrategyWhichShouldWork();
|
||||
}
|
||||
return choosenStrategy;
|
||||
}
|
||||
|
||||
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
|
||||
for (WebDavMounterStrategy strategy : STRATEGIES) {
|
||||
if (strategy.shouldWork()) {
|
||||
LOG.info("Using {}", strategy.getClass().getSimpleName());
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
return new FallbackWebDavMounter();
|
||||
}
|
||||
|
||||
private WebDavMounter() {
|
||||
throw new IllegalStateException("Class is not instantiable.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
|
||||
/**
|
||||
* A strategy able to mount a webdav share and display it to the user.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
interface WebDavMounterStrategy {
|
||||
|
||||
/**
|
||||
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
|
||||
*/
|
||||
boolean shouldWork();
|
||||
|
||||
/**
|
||||
* Tries to mount a given webdav share.
|
||||
*
|
||||
* @param localPort local TCP port of the webdav share
|
||||
* @return a {@link WebDavMount} representing the mounted share
|
||||
* @throws CommandFailedException if the mount operation fails
|
||||
*/
|
||||
WebDavMount mount(int localPort) throws CommandFailedException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*******************************************************************************
|
||||
* 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.ui.util.mount;
|
||||
|
||||
import static org.cryptomator.ui.util.command.Script.fromLines;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.CommandResult;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
|
||||
/**
|
||||
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
|
||||
* <p>
|
||||
* Tested on Windows 7 but should also work on Windows 8.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(int localPort) throws CommandFailedException {
|
||||
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort));
|
||||
final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS);
|
||||
final String driveLetter = getDriveLetter(mountResult.getStdOut());
|
||||
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
|
||||
return new WebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
unmountScript.execute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String getDriveLetter(String result) throws CommandFailedException {
|
||||
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
throw new CommandFailedException("Failed to get a drive letter from net use output.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import org.cryptomator.ui.controls.*?>
|
||||
|
||||
|
||||
<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.AccessController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
|
||||
<stylesheets>
|
||||
<URL value="@panels.css" />
|
||||
</stylesheets>
|
||||
|
||||
<padding>
|
||||
<Insets top="10" right="10" bottom="10" left="10" />
|
||||
</padding>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
|
||||
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
|
||||
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
<Button GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.username" GridPane.halignment="RIGHT" />
|
||||
<ComboBox fx:id="usernameBox" GridPane.rowIndex="1" GridPane.columnIndex="1" promptText="$access.label.username" disable="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%access.label.password" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" defaultButton="true" onAction="#startStopServer" focusTraversable="false" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
2977
main/ui/src/main/resources/css/linux_theme.css
Normal file
2977
main/ui/src/main/resources/css/linux_theme.css
Normal file
File diff suppressed because it is too large
Load Diff
886
main/ui/src/main/resources/css/mac_theme.css
Normal file
886
main/ui/src/main/resources/css/mac_theme.css
Normal file
@@ -0,0 +1,886 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
.root {
|
||||
-fx-font-family: 'lucida-grande';
|
||||
-fx-font-smoothing-type: lcd;
|
||||
-fx-font-size: 13.0;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* The main color palette from which the rest of the colors are derived. *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
-fx-base: #FFFFFF;
|
||||
-fx-background: #ECECEC;
|
||||
|
||||
/* Used for the inside of text boxes, password boxes, lists, trees, and
|
||||
* tables. See also -fx-text-inner-color, which should be used as the
|
||||
* -fx-text-fill value for text painted on top of backgrounds colored
|
||||
* with -fx-control-inner-background.
|
||||
*/
|
||||
-fx-control-inner-background: #FFFFFF;
|
||||
|
||||
/* One of these colors will be chosen based upon a ladder calculation
|
||||
* that uses the brightness of a background color. Instead of using these
|
||||
* colors directly as -fx-text-fill values, the sections in this file should
|
||||
* use a derived color to match the background in use. See also:
|
||||
*
|
||||
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
|
||||
* -fx-text-background-color for text on top of -fx-background
|
||||
* -fx-text-inner-color for text on top of -fx-control-inner-color
|
||||
* -fx-selection-bar-text for text on top of -fx-selection-bar
|
||||
*/
|
||||
-fx-dark-text-color: black;
|
||||
-fx-mid-text-color: #B5B5B5;
|
||||
-fx-light-text-color: white;
|
||||
|
||||
/* A bright blue for highlighting/accenting objects. For example: selected
|
||||
* text; selected items in menus, lists, trees, and tables; progress bars */
|
||||
-fx-accent: #B2D7FF;
|
||||
|
||||
/* A bright blue for the focus indicator of objects. Typically used as the
|
||||
* first color in -fx-background-color for the "focused" pseudo-class. Also
|
||||
* typically used with insets of -1.4 to provide a glowing effect.
|
||||
*/
|
||||
-fx-focus-color: #78A6D7;
|
||||
-fx-faint-focus-color: #8FBDEE;
|
||||
|
||||
/* The color that is used in styling controls. The default value is based
|
||||
* on -fx-base, but is changed by pseudoclasses to change the base color.
|
||||
* For example, the "hover" pseudoclass will typically set -fx-color to
|
||||
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
|
||||
* set -fx-color to -fx-pressed-base.
|
||||
*/
|
||||
-fx-color: -fx-base;
|
||||
|
||||
/* The opacity level to use for the "disabled" pseudoclass.
|
||||
*/
|
||||
-fx-disabled-opacity: 0.6;
|
||||
|
||||
/* Chart Color Palette */
|
||||
CHART_COLOR_1: #28CA40;
|
||||
CHART_COLOR_2: #FD4943;
|
||||
CHART_COLOR_3: #2283FB;
|
||||
CHART_COLOR_4: #FAEA77;
|
||||
CHART_COLOR_5: #FA9E78;
|
||||
CHART_COLOR_6: #F47BF8;
|
||||
CHART_COLOR_7: #c84164;
|
||||
CHART_COLOR_8: #888888;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Colors that are derived from the main color palette. *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/* The color to use for -fx-text-fill when text is to be painted on top of
|
||||
* a background filled with the -fx-background color.
|
||||
*/
|
||||
-fx-text-background-color: -fx-dark-text-color;
|
||||
|
||||
/* A little darker than -fx-color and used to draw boxes around objects such
|
||||
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
|
||||
*/
|
||||
-fx-box-border: #C8C8C8;
|
||||
|
||||
/* Darker than -fx-background and used to draw boxes around text boxes and
|
||||
* password boxes.
|
||||
*/
|
||||
-fx-text-box-border: #B5B5B5;
|
||||
|
||||
/* A gradient that goes from a little darker than -fx-color on the top to
|
||||
* even more darker than -fx-color on the bottom. Typically is the second
|
||||
* color in the -fx-background-color list as the small thin border around
|
||||
* a control. It is typically the same size as the control (i.e., insets
|
||||
* are 0).
|
||||
*/
|
||||
-fx-outer-border: derive(-fx-color,-23%);
|
||||
|
||||
/* A gradient that goes from a bit lighter than -fx-color on the top to
|
||||
* a little darker at the bottom. Typically is the third color in the
|
||||
* -fx-background-color list as a thin highlight inside the outer border.
|
||||
* Insets are typically 1.
|
||||
*/
|
||||
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
|
||||
|
||||
/* A gradient that goes from a little lighter than -fx-color at the top to
|
||||
* a little darker than -fx-color at the bottom and is used to fill the
|
||||
* body of many controls such as buttons. Typically is the fourth color
|
||||
* in the -fx-background-color list and represents main body of the control.
|
||||
* Insets are typically 2.
|
||||
*/
|
||||
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
|
||||
*/
|
||||
-fx-text-base-color: -fx-dark-text-color;
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-control-inner-background.
|
||||
*/
|
||||
-fx-text-inner-color: -fx-dark-text-color;
|
||||
|
||||
/* Background for items in list like things such as menus, lists, trees,
|
||||
* and tables.
|
||||
*
|
||||
* TODO: it seems like this should be based upon -fx-accent and we should
|
||||
* remove the setting -fx-background in all the sections that use
|
||||
* -fx-selection-bar.
|
||||
*/
|
||||
-fx-selection-bar: #2283FB;
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-selection-bar.
|
||||
*
|
||||
* TODO: it seems like this should be derived from -fx-selection-bar.
|
||||
*/
|
||||
-fx-selection-bar-text: -fx-light-text-color;
|
||||
|
||||
/* These are needed for Popup */
|
||||
-fx-background-color: inherit;
|
||||
-fx-background-radius: inherit;
|
||||
-fx-background-insets: inherit;
|
||||
-fx-padding: inherit;
|
||||
|
||||
-fx-cell-focus-inner-border: -fx-selection-bar;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Set the default background color for the scene *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
-fx-background-color: -fx-background;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Common Styles *
|
||||
* *
|
||||
* These are styles that give a standard look to a whole range of controls *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* ==== BUTTON LIKE THINGS ============================================== */
|
||||
|
||||
.button,
|
||||
.toggle-button,
|
||||
.radio-button > .radio,
|
||||
.check-box > .box,
|
||||
.menu-button,
|
||||
.choice-box,
|
||||
.color-picker.split-button > .color-picker-label,
|
||||
.combo-box-base,
|
||||
.combo-box-base > .arrow-button {
|
||||
-fx-background-color: linear-gradient(to bottom, #C1C1C1 0%, #A6A6A6 100%), -fx-base;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-background-radius: 4;
|
||||
-fx-padding: 0.2em 0.8em 0.2em 0.8em;
|
||||
-fx-text-fill: -fx-dark-text-color;
|
||||
-fx-alignment: CENTER;
|
||||
-fx-focus-traversable: false;
|
||||
-fx-effect: dropshadow(one-pass-box, #DCDCDC, 0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
.button:hover,
|
||||
.toggle-button:hover,
|
||||
.radio-button:hover > .radio,
|
||||
.check-box:hover > .box,
|
||||
.menu-button:hover,
|
||||
.split-menu-button > .label:hover,
|
||||
.split-menu-button > .arrow-button:hover,
|
||||
.slider .thumb:hover,
|
||||
.scroll-bar > .thumb:hover,
|
||||
.scroll-bar > .increment-button:hover,
|
||||
.scroll-bar > .decrement-button:hover,
|
||||
.choice-box:hover,
|
||||
.color-picker.split-button > .arrow-button:hover,
|
||||
.color-picker.split-button > .color-picker-label:hover,
|
||||
.combo-box-base:hover,
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
|
||||
-fx-color: -fx-base;
|
||||
}
|
||||
.button:armed,
|
||||
.button:default:armed,
|
||||
.toggle-button:armed,
|
||||
.radio-button:armed > .radio,
|
||||
.check-box:armed .box,
|
||||
.menu-button:armed,
|
||||
.split-menu-button:armed > .label,
|
||||
.split-menu-button > .arrow-button:pressed,
|
||||
.split-menu-button:showing > .arrow-button,
|
||||
.slider .thumb:pressed,
|
||||
.scroll-bar > .thumb:pressed,
|
||||
.scroll-bar > .increment-button:pressed,
|
||||
.scroll-bar > .decrement-button:pressed,
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
|
||||
-fx-background-color: linear-gradient(to bottom, #237FFE 0%, #023FDD 100%), linear-gradient(to bottom, #4A97FD 0%, #0867E4 100%);
|
||||
-fx-text-fill: -fx-light-text-color;
|
||||
}
|
||||
.button:focused,
|
||||
.toggle-button:focused,
|
||||
.radio-button:focused > .radio,
|
||||
.check-box:focused > .box,
|
||||
.menu-button:focused,
|
||||
.choice-box:focused,
|
||||
.color-picker.split-button:focused > .color-picker-label {
|
||||
-fx-background-color: -fx-faint-focus-color, -fx-focus-color, -fx-inner-border, -fx-body-color;
|
||||
-fx-background-insets: -2, -0.3, 1, 2;
|
||||
-fx-background-radius: 7, 6, 4, 3;
|
||||
}
|
||||
|
||||
/* ==== DISABLED THINGS ================================================= */
|
||||
|
||||
.button:disabled,
|
||||
.toggle-button:disabled,
|
||||
.radio-button:disabled,
|
||||
.check-box:disabled,
|
||||
.hyperlink:disabled,
|
||||
.menu-button:disabled,
|
||||
.split-menu-button:disabled,
|
||||
.slider:disabled,
|
||||
.scroll-pane:disabled,
|
||||
.progress-bar:disabled,
|
||||
.progress-indicator:disabled,
|
||||
.text-input:disabled,
|
||||
.choice-box:disabled,
|
||||
.combo-box-base:disabled,
|
||||
.list-view:disabled,
|
||||
.tree-view:disabled,
|
||||
.table-view:disabled,
|
||||
.tree-table-view:disabled,
|
||||
.tab-pane:disabled,
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
|
||||
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-text-fill: -fx-mid-text-color;
|
||||
-fx-effect: dropshadow(one-pass-box, #E0E0E0, 0.0, 0.0, 0.0, 0.5);
|
||||
}
|
||||
|
||||
/* ==== MNEMONIC THINGS ================================================= */
|
||||
|
||||
.button:show-mnemonics .mnemonic-underline,
|
||||
.toggle-button:show-mnemonics .mnemonic-underline,
|
||||
.radio-button:show-mnemonics .mnemonic-underline,
|
||||
.check-box:show-mnemonics .mnemonic-underline,
|
||||
.hyperlink:show-mnemonics > .mnemonic-underline,
|
||||
.split-menu-button:show-mnemonics > .mnemonic-underline,
|
||||
.menu-button:show-mnemonics > .mnemonic-underline {
|
||||
-fx-stroke: -fx-text-base-color;
|
||||
}
|
||||
|
||||
/* ==== ARROWS ========================================================== */
|
||||
|
||||
.combo-box-base > .arrow-button > .arrow {
|
||||
-fx-background-color: -fx-light-text-color;
|
||||
-fx-background-insets: 0 0 -1 0, 0;
|
||||
-fx-padding: 9px 6px 0 0;
|
||||
-fx-shape: "M 0 3 l 3 -3 l 3 3 m 0 3 l -3 3 l -3 -3";
|
||||
}
|
||||
|
||||
/* ==== CHOICE BOX LIKE THINGS ========================================== */
|
||||
|
||||
.combo-box-base {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* ==== BOX LIKE THINGS ================================================= */
|
||||
|
||||
.scroll-pane,
|
||||
.split-pane,
|
||||
.list-view,
|
||||
.tree-view,
|
||||
.table-view,
|
||||
.tree-table-view {
|
||||
-fx-background-color: -fx-box-border, -fx-control-inner-background;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-padding: 1;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Label *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.label {
|
||||
-fx-text-fill: -fx-text-background-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Button & ToggleButton *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* ==== DEFAULT ========================================================= */
|
||||
|
||||
.button:default {
|
||||
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
|
||||
-fx-text-fill: -fx-light-text-color;
|
||||
}
|
||||
.button:default:disabled {
|
||||
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-text-fill: -fx-mid-text-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ToolBar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.tool-bar:horizontal {
|
||||
-fx-background-color: -fx-box-border, -fx-background;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0.4em;
|
||||
-fx-spacing: 0.2em;
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
}
|
||||
|
||||
.tool-bar:horizontal > .container > .separator {
|
||||
-fx-orientation: vertical;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ScrollBar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
.scroll-bar:horizontal,
|
||||
.scroll-bar:vertical {
|
||||
-fx-background-color: #E8E8E8, #FAFAFA;
|
||||
}
|
||||
|
||||
.scroll-bar:disabled {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
.scroll-bar > .thumb {
|
||||
-fx-background-color: #C1C1C1;
|
||||
-fx-background-insets: 2px;
|
||||
-fx-background-radius: 4px;
|
||||
}
|
||||
.scroll-bar > .thumb:hover {
|
||||
-fx-background-color: #7D7D7D;
|
||||
}
|
||||
|
||||
.scroll-bar > .increment-button,
|
||||
.scroll-bar > .decrement-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal > .increment-button,
|
||||
.scroll-bar:horizontal > .decrement-button {
|
||||
-fx-padding: 6px 0px;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical > .increment-button,
|
||||
.scroll-bar:vertical > .decrement-button {
|
||||
-fx-padding: 0px 6px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Separator *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.separator:horizontal .line {
|
||||
-fx-border-color: -fx-text-box-border transparent transparent transparent;
|
||||
-fx-border-insets: 0, 1 0 0 0;
|
||||
}
|
||||
.separator:vertical .line {
|
||||
-fx-border-color: transparent transparent transparent -fx-text-box-border;
|
||||
-fx-border-width: 3, 1;
|
||||
-fx-border-insets: 0, 0 0 0 1;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ProgressIndicator *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.progress-indicator {
|
||||
-fx-indeterminate-segment-count: 12.0;
|
||||
}
|
||||
|
||||
.progress-indicator > .determinate-indicator > .indicator {
|
||||
-fx-background-color:
|
||||
rgb(208.0, 208.0, 208.0),
|
||||
linear-gradient(rgb(176.0, 176.0, 176.0), rgb(207.0, 207.0, 207.0)),
|
||||
linear-gradient(rgb(190.0, 190.0, 190.0) 0.0%, rgb(213.0, 213.0, 213.0) 15.0%, rgb(230.0, 230.0, 230.0) 50.0%, rgb(235.0, 235.0, 235.0) 100.0%),
|
||||
linear-gradient(to left, rgb(196.0, 196.0, 196.0, 0.5) 0.0%, rgb(220.0, 220.0, 220.0, 0.2) 2.0% , transparent);
|
||||
-fx-background-insets: 0.5 0.0 -0.5 0.0, 0.0, 0.5, 1.0;
|
||||
-fx-padding: 0.083333em;
|
||||
}
|
||||
|
||||
.progress-indicator > .determinate-indicator > .progress {
|
||||
-fx-background-color:
|
||||
rgb(208.0, 208.0, 208.0),
|
||||
radial-gradient(center 50.0% 50.0%, radius 50.0%, rgb(84.0, 170.0, 240.0), rgb(90.0, 192.0, 246.0));
|
||||
-fx-background-insets: 0.0, 0.5;
|
||||
-fx-padding: 0.166667em;
|
||||
}
|
||||
|
||||
.progress-indicator > .determinate-indicator > .tick {
|
||||
-fx-background-color: rgb(208.0, 208.0, 208.0), white;
|
||||
-fx-background-insets: 1.0 0.0 -1.0 0.0, 0.0;
|
||||
-fx-padding: 0.416667em;
|
||||
-fx-shape: "m 18.174523,1027.137 c -0.18077,-0.4575 -0.184364,-0.8913 0.115901,-1.1721 0.300265,-0.2809 0.836622,-0.3601 1.288422,-0.041 0.4518,0.3191 2.020453,2.9316 2.020453,2.9316 l 5.41194,-8.0232 c -4e-6,0 0.516257,-0.6671 1.248682,-0.3099 0.648831,0.3165 0.559153,1.0373 0.559153,1.0373 0,0 -5.940433,9.3556 -6.15501,9.6287 -0.214577,0.273 -1.595078,0.2481 -1.817995,-0.027 -0.222917,-0.2751 -2.490777,-3.567 -2.671546,-4.0244 z";
|
||||
-fx-scale-shape: false;
|
||||
}
|
||||
|
||||
.progress-indicator > .percentage {
|
||||
-fx-font-size: 0.916667em;
|
||||
}
|
||||
|
||||
.progress-indicator:disabled {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
|
||||
.progress-indicator:indeterminate > .spinner {
|
||||
-fx-padding: 0.083333em;
|
||||
}
|
||||
|
||||
.progress-indicator:indeterminate .segment {
|
||||
-fx-background-color: rgb(95.0, 95.0, 98.0), rgb(122.0, 122.0, 125.0);
|
||||
-fx-background-insets:0.0, 0.5;
|
||||
}
|
||||
.progress-indicator:indeterminate .segment0 {
|
||||
-fx-shape:"m 14.321262,6.5816808 c -0.824944,0.3797564 -0.10368,1.8484772 0.718513,1.3544717 L 18.786514,5.9486042 C 19.644992,5.4932031 18.92648,4.1387308 18.068001,4.5941315 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment1 {
|
||||
-fx-shape:"m 15.372451,9.2445322 c -0.906719,-0.051108 -0.957826,1.5843588 0,1.5332498 l 4.241273,0 c 0.97179,0 0.97179,-1.5332498 0,-1.5332498 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment2 {
|
||||
-fx-shape:"m 14.423504,13.443113 c -0.824945,-0.379757 -0.10368,-1.848478 0.718512,-1.354472 l 3.746739,1.987548 c 0.858478,0.455401 0.139967,1.809873 -0.718512,1.354473 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment3 {
|
||||
-fx-shape:"m 12.10997,15.070611 c -0.49762,-0.759687 0.893182,-1.621681 1.327834,-0.766626 l 2.120636,3.673051 c 0.485895,0.841595 -0.841938,1.60822 -1.327833,0.766625 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment4 {
|
||||
-fx-shape:"m 9.2224559,19.539943 c -0.051108,0.906718 1.5843581,0.957826 1.5332501,0 l 0,-4.241273 c 0,-0.97179 -1.5332501,-0.97179 -1.5332501,0 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment5 {
|
||||
-fx-shape:"M 8.0465401,15.070611 C 8.5441601,14.310924 7.1533584,13.44893 6.7187068,14.303985 l -2.1206366,3.673051 c -0.485895,0.841595 0.8419383,1.60822 1.3278333,0.766625 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment6 {
|
||||
-fx-shape:"M 5.7330066,13.443113 C 6.5579512,13.063356 5.8366865,11.594635 5.0144939,12.088641 L 1.2677551,14.076189 C 0.409277,14.53159 1.1277888,15.886062 1.9862674,15.430662 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment7 {
|
||||
-fx-shape:"m 0.42171041,9.2083842 c -0.90671825,-0.051108 -0.95782608,1.5843588 0,1.5332498 l 4.24127319,0 c 0.9717899,0 0.9717899,-1.5332498 0,-1.5332498 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment8 {
|
||||
-fx-shape:"M 5.7330066,6.5305598 C 6.5579512,6.9103162 5.8366865,8.3790371 5.0144939,7.8850315 L 1.2677551,5.8974832 C 0.409277,5.4420823 1.1277888,4.0876101 1.9862674,4.5430105 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment9 {
|
||||
-fx-shape:"M 8.0465401,4.9030617 C 8.5441601,5.6627485 7.1533584,6.5247425 6.7187068,5.6696872 L 4.5980702,1.9966363 C 4.1121752,1.1550418 5.4400085,0.38841683 5.9259035,1.2300114 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment10 {
|
||||
-fx-shape:"m 9.2224559,4.62535 c -0.051108,0.9067177 1.5843581,0.957826 1.5332501,0 l 0,-4.24127319 c 0,-0.9717899 -1.5332501,-0.9717899 -1.5332501,0 z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment11 {
|
||||
-fx-shape:"m 12.007729,4.9541827 c -0.49762,0.7596865 0.893181,1.6216808 1.327833,0.7666252 L 15.456199,2.0477574 C 15.942094,1.2061627 14.61426,0.43953765 14.128365,1.2811324 z";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Text COMMON *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.text-input {
|
||||
-fx-text-fill: -fx-dark-text-color;
|
||||
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
|
||||
-fx-highlight-text-fill: -fx-text-inner-color;
|
||||
-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);
|
||||
-fx-border-color: -fx-text-box-border;
|
||||
-fx-border-width: 1px;
|
||||
-fx-background-color: #FFFFFF;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 0;
|
||||
-fx-cursor: text;
|
||||
-fx-padding: 2px;
|
||||
}
|
||||
.text-input:focused {
|
||||
-fx-highlight-fill: -fx-accent;
|
||||
-fx-border-color: -fx-focus-color;
|
||||
-fx-border-width: 1px;
|
||||
-fx-background-color: -fx-faint-focus-color, #FFFFFF;
|
||||
-fx-background-insets: -3, 0;
|
||||
-fx-background-radius: 3, 0;
|
||||
-fx-prompt-text-fill: transparent;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* PopupMenu *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.context-menu {
|
||||
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 4.0;
|
||||
-fx-padding: 4px 0 4px 0;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0 );
|
||||
}
|
||||
.context-menu > .separator {
|
||||
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* MenuItem *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.menu-item {
|
||||
-fx-background-color: transparent;
|
||||
-fx-background-insets:0.0;
|
||||
-fx-padding:0.2em 1em 0.2em 1em;
|
||||
-fx-border-width: 0.0 0.0 0.0 0.0;
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
.menu-item > .left-container {
|
||||
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
|
||||
}
|
||||
.menu-item > .graphic-container {
|
||||
-fx-padding: 0em 0.333em 0em 0em;
|
||||
}
|
||||
.menu-item >.label {
|
||||
-fx-padding: 0em 0.5em 0em 0em;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
}
|
||||
.menu-item:disabled > .label {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
.menu-item:focused {
|
||||
-fx-background: -fx-accent;
|
||||
-fx-background-color: -fx-selection-bar;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
}
|
||||
.menu-item:focused > .label {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
.menu-item > .right-container {
|
||||
-fx-padding: 0em 0em 0em 0.5em;
|
||||
}
|
||||
.menu-item:show-mnemonics > .mnemonic-underline {
|
||||
-fx-stroke: -fx-text-fill;
|
||||
}
|
||||
.menu > .right-container > .arrow {
|
||||
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
|
||||
-fx-background-color: -fx-color;
|
||||
-fx-shape: "M0,-4L4,0L0,4Z";
|
||||
-fx-scale-shape: false;
|
||||
}
|
||||
.menu:selected > .right-container > .arrow {
|
||||
-fx-background-color: white;
|
||||
}
|
||||
.menu-item:disabled {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ComboBox *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Customie the ListCell that appears in the ComboBox button itself */
|
||||
.combo-box > .list-cell {
|
||||
-fx-background: transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
}
|
||||
.combo-box-base > .arrow-button {
|
||||
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
|
||||
-fx-background-radius: 0 5 5 0, 0 4 4 0;
|
||||
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
-fx-background-insets: 0 0 0 1, 1;
|
||||
}
|
||||
|
||||
.combo-box-popup > .list-view {
|
||||
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 4.0;
|
||||
-fx-padding: 4px 0 4px 0;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding:0.2em 1em 0.2em 1em;
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
|
||||
-fx-background: -fx-accent;
|
||||
-fx-background-color: -fx-selection-bar;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* SplitPane *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.split-pane > .split-pane-divider {
|
||||
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ListView and ListCell *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.list-view > .virtual-flow > .scroll-bar:vertical{
|
||||
-fx-background-insets: 0, 0 0 0 1;
|
||||
-fx-padding: -1 -1 -1 0;
|
||||
}
|
||||
.list-view > .virtual-flow > .scroll-bar:horizontal{
|
||||
-fx-background-insets: 0, 1 0 0 0;
|
||||
-fx-padding: 0 -1 -1 -1;
|
||||
}
|
||||
.list-view > .virtual-flow > .corner {
|
||||
-fx-background-color: -fx-box-border, -fx-base;
|
||||
-fx-background-insets: 0, 1 0 0 1;
|
||||
}
|
||||
.list-cell {
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
|
||||
-fx-text-fill: -fx-text-inner-color;
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused {
|
||||
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-control-inner-background;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
}
|
||||
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
|
||||
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-selection-bar;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
-fx-background: -fx-accent;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
}
|
||||
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
|
||||
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
|
||||
-fx-background: -fx-accent;
|
||||
-fx-background-color: -fx-selection-bar;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
}
|
||||
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
|
||||
-fx-background: -fx-accent;
|
||||
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-selection-bar;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
-fx-text-fill: -fx-selection-bar-text;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Tooltip *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.tooltip {
|
||||
-fx-background-color: -fx-background;
|
||||
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
|
||||
-fx-font-size: 0.8em;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Charts *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart {
|
||||
-fx-padding: 5px;
|
||||
}
|
||||
.chart-content {
|
||||
-fx-padding: 10px;
|
||||
}
|
||||
.chart-title {
|
||||
-fx-font-size: 1.4em;
|
||||
}
|
||||
.chart-legend {
|
||||
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
|
||||
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
|
||||
-fx-background-insets: 0,1;
|
||||
-fx-background-radius: 6,5;
|
||||
-fx-padding: 6px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Axis *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.axis {
|
||||
AXIS_COLOR: derive(-fx-background,-20%);
|
||||
-fx-tick-label-font-size: 0.833333em; /* 10px */
|
||||
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
|
||||
}
|
||||
.axis:top {
|
||||
-fx-border-color: transparent transparent AXIS_COLOR transparent;
|
||||
}
|
||||
.axis:right {
|
||||
-fx-border-color: transparent transparent transparent AXIS_COLOR;
|
||||
}
|
||||
.axis:bottom {
|
||||
-fx-border-color: AXIS_COLOR transparent transparent transparent;
|
||||
}
|
||||
.axis:left {
|
||||
-fx-border-color: transparent AXIS_COLOR transparent transparent;
|
||||
}
|
||||
.axis-tick-mark,
|
||||
.axis-minor-tick-mark {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: AXIS_COLOR;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ChartPlot *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart-vertical-grid-lines {
|
||||
-fx-stroke: derive(-fx-background,-10%);
|
||||
-fx-stroke-dash-array: 0.25em, 0.25em;
|
||||
}
|
||||
.chart-horizontal-grid-lines {
|
||||
-fx-stroke: derive(-fx-background,-10%);
|
||||
}
|
||||
.chart-alternative-column-fill {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: null;
|
||||
}
|
||||
.chart-alternative-row-fill {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: null;
|
||||
}
|
||||
.chart-vertical-zero-line,
|
||||
.chart-horizontal-zero-line {
|
||||
-fx-stroke: derive(-fx-text-background-color, 40%);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* LineChart *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart-line-symbol {
|
||||
-fx-background-color: #f9d900, white;
|
||||
-fx-background-insets: 0, 2;
|
||||
-fx-background-radius: 5px;
|
||||
-fx-padding: 5px;
|
||||
}
|
||||
.chart-series-line {
|
||||
-fx-stroke: #f9d900;
|
||||
-fx-stroke-width: 3px;
|
||||
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
|
||||
}
|
||||
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
|
||||
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
|
||||
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
|
||||
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
|
||||
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
|
||||
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
|
||||
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
|
||||
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
|
||||
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
|
||||
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
|
||||
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
|
||||
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
|
||||
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
|
||||
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
|
||||
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
|
||||
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Combinations *
|
||||
* *
|
||||
* This section is for special handling of when one control is nested inside *
|
||||
* another control. There are many cases where we would end up with ugly *
|
||||
* double borders that are fixed here. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.split-pane > * > .table-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .list-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .tree-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
|
||||
.split-pane > * > .split-pane {
|
||||
-fx-background-insets: 0, 0;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* ############################################################################
|
||||
# Workaround for RT-27627 #
|
||||
############################################################################ */
|
||||
|
||||
.choice-box > .open-button > .arrow { doh: true; }
|
||||
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
|
||||
.tree-table-view { doh: true; }
|
||||
.tree-table-view:focused { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .corner { doh: true; }
|
||||
.tree-table-row-cell:focused { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
|
||||
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
|
||||
.tree-table-row-cell:filled:selected:focused { doh: true; }
|
||||
.tree-table-row-cell:filled:selected { doh: true; }
|
||||
.tree-table-row-cell:selected:disabled { doh: true; }
|
||||
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
|
||||
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
|
||||
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
|
||||
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
|
||||
.tree-table-view:constrained-resize .filler { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
|
||||
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
|
||||
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
|
||||
.tree-table-cell:selected:disabled { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
|
||||
.tree-table-view .column-resize-line { doh: true; }
|
||||
.tree-table-view > .column-header-background { doh: true; }
|
||||
.tree-table-view .column-header { doh: true; }
|
||||
.tree-table-view .filler { doh: true; }
|
||||
.tree-table-view .column-header .sort-order{ doh: true; }
|
||||
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
|
||||
.tree-table-view .show-hide-column-image { doh: true; }
|
||||
.tree-table-view .column-drag-header { doh: true; }
|
||||
.tree-table-view .column-overlay { doh: true; }
|
||||
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
|
||||
.tree-table-view .empty-table { doh: true; }
|
||||
.axis-minor-tick-mark { doh: true; }
|
||||
.chart-horizontal-zero-line { doh: true; }
|
||||
.stacked-bar-chart:horizontal .chart-bar { doh: true; }
|
||||
874
main/ui/src/main/resources/css/win_theme.css
Normal file
874
main/ui/src/main/resources/css/win_theme.css
Normal file
@@ -0,0 +1,874 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
.root {
|
||||
-fx-font-family: 'Segoe UI Semibold';
|
||||
-fx-font-smoothing-type: lcd;
|
||||
-fx-font-size: 12.0;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* The main color palette from which the rest of the colors are derived. *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
-fx-base: #EAEAEA;
|
||||
-fx-background: #F0F0F0;
|
||||
|
||||
/* Used for the inside of text boxes, password boxes, lists, trees, and
|
||||
* tables. See also -fx-text-inner-color, which should be used as the
|
||||
* -fx-text-fill value for text painted on top of backgrounds colored
|
||||
* with -fx-control-inner-background.
|
||||
*/
|
||||
-fx-control-inner-background: #FFFFFF;
|
||||
|
||||
/* One of these colors will be chosen based upon a ladder calculation
|
||||
* that uses the brightness of a background color. Instead of using these
|
||||
* colors directly as -fx-text-fill values, the sections in this file should
|
||||
* use a derived color to match the background in use. See also:
|
||||
*
|
||||
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
|
||||
* -fx-text-background-color for text on top of -fx-background
|
||||
* -fx-text-inner-color for text on top of -fx-control-inner-color
|
||||
* -fx-selection-bar-text for text on top of -fx-selection-bar
|
||||
*/
|
||||
-fx-dark-text-color: black;
|
||||
-fx-mid-text-color: #8B8B8B;
|
||||
-fx-light-text-color: white;
|
||||
|
||||
/* A bright blue for highlighting/accenting objects. For example: selected
|
||||
* text; selected items in menus, lists, trees, and tables; progress bars */
|
||||
-fx-accent: #3399FF;
|
||||
|
||||
/* A bright blue for the focus indicator of objects. Typically used as the
|
||||
* first color in -fx-background-color for the "focused" pseudo-class. Also
|
||||
* typically used with insets of -1.4 to provide a glowing effect.
|
||||
*/
|
||||
-fx-focus-color: #3399FF;
|
||||
|
||||
/* The color that is used in styling controls. The default value is based
|
||||
* on -fx-base, but is changed by pseudoclasses to change the base color.
|
||||
* For example, the "hover" pseudoclass will typically set -fx-color to
|
||||
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
|
||||
* set -fx-color to -fx-pressed-base.
|
||||
*/
|
||||
-fx-color: -fx-base;
|
||||
|
||||
/* The opacity level to use for the "disabled" pseudoclass.
|
||||
*/
|
||||
-fx-disabled-opacity: 0.6;
|
||||
|
||||
/* Chart Color Palette */
|
||||
CHART_COLOR_1: #A1CD5f;
|
||||
CHART_COLOR_2: #C75050;
|
||||
CHART_COLOR_3: #3399FF;
|
||||
CHART_COLOR_4: #FAEA77;
|
||||
CHART_COLOR_5: #FA9E78;
|
||||
CHART_COLOR_6: #F47BF8;
|
||||
CHART_COLOR_7: #c84164;
|
||||
CHART_COLOR_8: #888888;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Colors that are derived from the main color palette. *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/* The color to use for -fx-text-fill when text is to be painted on top of
|
||||
* a background filled with the -fx-background color.
|
||||
*/
|
||||
-fx-text-background-color: -fx-dark-text-color;
|
||||
|
||||
/* A little darker than -fx-color and used to draw boxes around objects such
|
||||
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
|
||||
*/
|
||||
-fx-box-border: #ACACAC;
|
||||
|
||||
/* Darker than -fx-background and used to draw boxes around text boxes and
|
||||
* password boxes.
|
||||
*/
|
||||
-fx-text-box-border: #ACACAC;
|
||||
|
||||
/* A gradient that goes from a little darker than -fx-color on the top to
|
||||
* even more darker than -fx-color on the bottom. Typically is the second
|
||||
* color in the -fx-background-color list as the small thin border around
|
||||
* a control. It is typically the same size as the control (i.e., insets
|
||||
* are 0).
|
||||
*/
|
||||
-fx-outer-border: derive(-fx-color,-23%);
|
||||
|
||||
/* A gradient that goes from a bit lighter than -fx-color on the top to
|
||||
* a little darker at the bottom. Typically is the third color in the
|
||||
* -fx-background-color list as a thin highlight inside the outer border.
|
||||
* Insets are typically 1.
|
||||
*/
|
||||
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
|
||||
|
||||
/* A gradient that goes from a little lighter than -fx-color at the top to
|
||||
* a little darker than -fx-color at the bottom and is used to fill the
|
||||
* body of many controls such as buttons. Typically is the fourth color
|
||||
* in the -fx-background-color list and represents main body of the control.
|
||||
* Insets are typically 2.
|
||||
*/
|
||||
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
|
||||
*/
|
||||
-fx-text-base-color: -fx-dark-text-color;
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-control-inner-background.
|
||||
*/
|
||||
-fx-text-inner-color: -fx-dark-text-color;
|
||||
|
||||
/* Background for items in list like things such as menus, lists, trees,
|
||||
* and tables.
|
||||
*
|
||||
* TODO: it seems like this should be based upon -fx-accent and we should
|
||||
* remove the setting -fx-background in all the sections that use
|
||||
* -fx-selection-bar.
|
||||
*/
|
||||
-fx-selection-bar: #3399FF;
|
||||
|
||||
/* The color to use as -fx-text-fill when painting text on top of
|
||||
* backgrounds filled with -fx-selection-bar.
|
||||
*
|
||||
* TODO: it seems like this should be derived from -fx-selection-bar.
|
||||
*/
|
||||
-fx-selection-bar-text: -fx-light-text-color;
|
||||
|
||||
/* These are needed for Popup */
|
||||
-fx-background-color: inherit;
|
||||
-fx-background-radius: inherit;
|
||||
-fx-background-insets: inherit;
|
||||
-fx-padding: inherit;
|
||||
|
||||
-fx-cell-focus-inner-border: -fx-selection-bar;
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Set the default background color for the scene *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
-fx-background-color: -fx-background;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Common Styles *
|
||||
* *
|
||||
* These are styles that give a standard look to a whole range of controls *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* ==== BUTTON LIKE THINGS ============================================== */
|
||||
|
||||
.button,
|
||||
.toggle-button,
|
||||
.radio-button > .radio,
|
||||
.check-box > .box,
|
||||
.menu-button,
|
||||
.choice-box,
|
||||
.color-picker.split-button > .color-picker-label,
|
||||
.combo-box-base,
|
||||
.combo-box-base > .arrow-button {
|
||||
-fx-background-color: -fx-box-border, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-background-radius: 0, 0;
|
||||
-fx-padding: 0.1em 0.6em 0.1em 0.6em;
|
||||
-fx-text-fill: -fx-dark-text-color;
|
||||
-fx-alignment: CENTER;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-insets: 2px;
|
||||
}
|
||||
.button:hover,
|
||||
.toggle-button:hover,
|
||||
.radio-button:hover > .radio,
|
||||
.check-box:hover > .box,
|
||||
.menu-button:hover,
|
||||
.split-menu-button > .label:hover,
|
||||
.split-menu-button > .arrow-button:hover,
|
||||
.slider .thumb:hover,
|
||||
.choice-box:hover,
|
||||
.color-picker.split-button > .arrow-button:hover,
|
||||
.color-picker.split-button > .color-picker-label:hover,
|
||||
.combo-box-base:hover,
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
|
||||
-fx-color: -fx-base;
|
||||
}
|
||||
.button:armed,
|
||||
.button:default:armed,
|
||||
.toggle-button:armed,
|
||||
.radio-button:armed > .radio,
|
||||
.check-box:armed .box,
|
||||
.menu-button:armed,
|
||||
.split-menu-button:armed > .label,
|
||||
.split-menu-button > .arrow-button:pressed,
|
||||
.split-menu-button:showing > .arrow-button,
|
||||
.slider .thumb:pressed,
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
|
||||
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
|
||||
|
||||
}
|
||||
.button:focused,
|
||||
.toggle-button:focused,
|
||||
.radio-button:focused > .radio,
|
||||
.check-box:focused > .box,
|
||||
.menu-button:focused,
|
||||
.choice-box:focused,
|
||||
.color-picker.split-button:focused > .color-picker-label {
|
||||
-fx-border-color: black;
|
||||
-fx-border-insets: 2px;
|
||||
-fx-border-style: dotted inside;
|
||||
}
|
||||
|
||||
/* ==== DISABLED THINGS ================================================= */
|
||||
|
||||
.button:disabled,
|
||||
.toggle-button:disabled,
|
||||
.radio-button:disabled,
|
||||
.check-box:disabled,
|
||||
.hyperlink:disabled,
|
||||
.menu-button:disabled,
|
||||
.split-menu-button:disabled,
|
||||
.slider:disabled,
|
||||
.scroll-pane:disabled,
|
||||
.progress-bar:disabled,
|
||||
.progress-indicator:disabled,
|
||||
.text-input:disabled,
|
||||
.choice-box:disabled,
|
||||
.combo-box-base:disabled,
|
||||
.list-view:disabled,
|
||||
.tree-view:disabled,
|
||||
.table-view:disabled,
|
||||
.tree-table-view:disabled,
|
||||
.tab-pane:disabled,
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
|
||||
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
|
||||
-fx-text-fill: -fx-mid-text-color;
|
||||
}
|
||||
|
||||
/* ==== MNEMONIC THINGS ================================================= */
|
||||
|
||||
.button:show-mnemonics .mnemonic-underline,
|
||||
.toggle-button:show-mnemonics .mnemonic-underline,
|
||||
.radio-button:show-mnemonics .mnemonic-underline,
|
||||
.check-box:show-mnemonics .mnemonic-underline,
|
||||
.hyperlink:show-mnemonics > .mnemonic-underline,
|
||||
.split-menu-button:show-mnemonics > .mnemonic-underline,
|
||||
.menu-button:show-mnemonics > .mnemonic-underline {
|
||||
-fx-stroke: -fx-text-base-color;
|
||||
}
|
||||
|
||||
/* ==== ARROWS ========================================================== */
|
||||
|
||||
.combo-box-base > .arrow-button > .arrow {
|
||||
-fx-background-color: #606060;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 2px 4px 2px 4px;
|
||||
-fx-shape: "M 0 0 h 7 l -3.5 4 z";
|
||||
}
|
||||
|
||||
/* ==== CHOICE BOX LIKE THINGS ========================================== */
|
||||
|
||||
.combo-box-base {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* ==== BOX LIKE THINGS ================================================= */
|
||||
|
||||
.scroll-pane,
|
||||
.split-pane,
|
||||
.list-view,
|
||||
.tree-view,
|
||||
.table-view,
|
||||
.tree-table-view {
|
||||
-fx-background-color: -fx-box-border, -fx-control-inner-background;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-padding: 1;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Label *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.label {
|
||||
-fx-text-fill: -fx-text-background-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Button & ToggleButton *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* ==== DEFAULT ========================================================= */
|
||||
|
||||
.button:default {
|
||||
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
|
||||
}
|
||||
.button:default:disabled {
|
||||
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
|
||||
-fx-text-fill: -fx-mid-text-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ToolBar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.tool-bar:horizontal {
|
||||
-fx-background-color: -fx-box-border, -fx-background;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0.4em;
|
||||
-fx-spacing: 0.2em;
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
}
|
||||
|
||||
.tool-bar:horizontal > .container > .separator {
|
||||
-fx-orientation: vertical;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ScrollBar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
.scroll-bar:horizontal,
|
||||
.scroll-bar:vertical {
|
||||
-fx-background-color: -fx-base;
|
||||
}
|
||||
|
||||
.scroll-bar:disabled {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
.scroll-bar > .thumb {
|
||||
-fx-background-color: #CDCDCD;
|
||||
}
|
||||
.scroll-bar > .thumb:hover {
|
||||
-fx-background-color: #A6A6A6;
|
||||
}
|
||||
|
||||
.scroll-bar > .increment-button,
|
||||
.scroll-bar > .decrement-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal > .increment-button,
|
||||
.scroll-bar:horizontal > .decrement-button {
|
||||
-fx-padding: 5px 5px;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical > .increment-button,
|
||||
.scroll-bar:vertical > .decrement-button {
|
||||
-fx-padding: 5px 5px;
|
||||
}
|
||||
|
||||
.scroll-bar > .increment-button,
|
||||
.scroll-bar > .decrement-button {
|
||||
-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
-fx-color: transparent;
|
||||
-fx-padding: 3px;
|
||||
}
|
||||
.scroll-bar > .increment-button > .increment-arrow,
|
||||
.scroll-bar > .decrement-button > .decrement-arrow {
|
||||
-fx-background-color: #606060;
|
||||
}
|
||||
.scroll-bar > .increment-button:hover > .increment-arrow,
|
||||
.scroll-bar > .decrement-button:hover > .decrement-arrow {
|
||||
-fx-background-color: #606060;
|
||||
}
|
||||
.scroll-bar > .increment-button:pressed > .increment-arrow,
|
||||
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
|
||||
-fx-background-color: #606060;
|
||||
}
|
||||
.scroll-bar:horizontal > .increment-button > .increment-arrow {
|
||||
-fx-padding: 9 7 0 0;
|
||||
-fx-shape: "M0.315,1.457l1.414-1.414L5.686,4L1.729,7.957L0.315,6.543L2.857,4L0.315,1.457z";
|
||||
}
|
||||
.scroll-bar:vertical > .increment-button > .increment-arrow {
|
||||
-fx-padding: 7 9 0 0 ;
|
||||
-fx-shape: "M6.543,0.315l1.414,1.414L4,5.686L0.043,1.729l1.414-1.414L4,2.858L6.543,0.315z";
|
||||
}
|
||||
.scroll-bar:horizontal > .decrement-button > .decrement-arrow {
|
||||
-fx-padding: 9 7 0 0;
|
||||
-fx-shape: "M5.686,6.543L4.271,7.957L0.314,4l3.957-3.957l1.414,1.414L3.143,4L5.686,6.543z";
|
||||
}
|
||||
.scroll-bar:vertical > .decrement-button > .decrement-arrow {
|
||||
-fx-padding: 7 9 0 0;
|
||||
-fx-shape: "M1.457,5.563L0.042,4.149L4,0.193l3.957,3.957L6.543,5.563L4,3.021L1.457,5.563z";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Separator *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.separator:horizontal .line {
|
||||
-fx-border-color: -fx-text-box-border transparent transparent transparent;
|
||||
-fx-border-insets: 0, 1 0 0 0;
|
||||
}
|
||||
.separator:vertical .line {
|
||||
-fx-border-color: transparent transparent transparent -fx-text-box-border;
|
||||
-fx-border-width: 3, 1;
|
||||
-fx-border-insets: 0, 0 0 0 1;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ProgressIndicator *
|
||||
* *
|
||||
******************************************************************************/
|
||||
.progress-indicator {
|
||||
-fx-indeterminate-segment-count: 12;
|
||||
-fx-spin-enabled: true;
|
||||
}
|
||||
.progress-indicator > .determinate-indicator > .indicator {
|
||||
-fx-background-color: -fx-box-border,
|
||||
radial-gradient(center 50% 50%, radius 50%, -fx-control-inner-background 70%, derive(-fx-control-inner-background, -9%) 100%),
|
||||
-fx-control-inner-background;
|
||||
-fx-background-insets: 0, 1, 5 2 1 2;
|
||||
-fx-padding: 1;
|
||||
}
|
||||
.progress-indicator > .determinate-indicator > .progress {
|
||||
-fx-background-color: -fx-accent;
|
||||
-fx-background-insets: 2;
|
||||
-fx-padding: 1em; /* 9 */
|
||||
}
|
||||
/* TODO: scaling the shape seems to make it disappear */
|
||||
.progress-indicator > .determinate-indicator > .tick {
|
||||
-fx-background-color: white;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0.416667em; /* 5 */
|
||||
-fx-shape: "M-0.25,6.083c0.843-0.758,4.583,4.833,5.75,4.833S14.5-1.5,15.917-0.917c1.292,0.532-8.75,17.083-10.5,17.083C3,16.167-1.083,6.833-0.25,6.083z";
|
||||
-fx-scale-shape: false;
|
||||
}
|
||||
.progress-indicator:indeterminate > .spinner {
|
||||
-fx-padding: 0.833333em; /* 10 */
|
||||
}
|
||||
.progress-indicator > .percentage {
|
||||
-fx-font-size: 0.916667em; /* 11pt - 1 less than the default font */
|
||||
-fx-fill: -fx-text-background-color;
|
||||
}
|
||||
.progress-indicator:indeterminate .segment {
|
||||
-fx-background-color: -fx-accent;
|
||||
}
|
||||
.progress-indicator:indeterminate .segment0 {
|
||||
-fx-shape:"M10,0C9.998,0,9.995,0,9.992,0C9.991,0,9.991,0,9.99,0C9.988,0,9.986,0,9.985,0S9.982,0,9.981,0S9.979,0,9.978,0S9.975,0,9.974,0S9.972,0,9.971,0C9.969,0,9.968,0,9.966,0H9.965c-0.007,0-0.014,0-0.02,0C9.944,0,9.944,0,9.944,0C9.941,0,9.939,0,9.937,0c-0.77,0.007-1.389,0.634-1.384,1.404C8.557,2.176,9.185,2.8,9.956,2.8c0.001,0,0.003,0,0.004,0H10c0.773-0.002,1.4-0.63,1.4-1.404c0-0.77-0.622-1.393-1.392-1.396C10.006,0,10.003,0,10,0L10,0z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment1 {
|
||||
-fx-shape:"M5.688,1.156c-0.236,0-0.476,0.06-0.696,0.187C4.98,1.349,4.969,1.356,4.958,1.363c0,0-0.001,0-0.001,0C4.955,1.364,4.954,1.365,4.952,1.366c-0.001,0-0.002,0.001-0.004,0.002c0,0,0,0-0.001,0C4.944,1.371,4.94,1.373,4.936,1.375c0,0,0,0-0.001,0C4.933,1.377,4.931,1.378,4.929,1.38C4.267,1.772,4.046,2.624,4.438,3.288c0.261,0.444,0.73,0.692,1.212,0.692c0.24,0,0.484-0.062,0.706-0.192l0.034-0.02C7.058,3.378,7.283,2.52,6.896,1.851C6.636,1.405,6.168,1.156,5.688,1.156L5.688,1.156z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment2 {
|
||||
-fx-shape:"M2.534,4.326c-0.482,0-0.951,0.25-1.209,0.697C1.323,5.027,1.321,5.029,1.32,5.031l0,0C1.319,5.033,1.318,5.034,1.317,5.036S1.315,5.039,1.314,5.04c0,0.001,0,0.002-0.001,0.002C1.312,5.044,1.311,5.046,1.31,5.048c0,0,0,0,0,0.001C1.309,5.051,1.308,5.053,1.307,5.055C1.302,5.063,1.297,5.071,1.292,5.079l0,0C1.291,5.082,1.29,5.084,1.288,5.087c-0.376,0.67-0.141,1.519,0.529,1.898c0.218,0.123,0.456,0.182,0.69,0.182c0.488,0,0.963-0.255,1.222-0.708l0.02-0.035c0.383-0.671,0.149-1.527-0.521-1.912C3.008,4.386,2.769,4.326,2.534,4.326L2.534,4.326z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment3 {
|
||||
-fx-shape:"M1.396,8.648c-0.002,0-0.005,0-0.008,0C0.619,8.652-0.001,9.278,0,10.047c0,0.002,0,0.006,0,0.008l0,0c0,0.019,0,0.037,0,0.056c0,0.001,0,0.002,0,0.003s0,0.003,0,0.004c0.01,0.765,0.632,1.378,1.396,1.378c0.005,0,0.01,0,0.015,0c0.773-0.009,1.395-0.642,1.389-1.415v-0.04C2.794,9.27,2.166,8.648,1.396,8.648L1.396,8.648z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment4 {
|
||||
-fx-shape:"M2.579,12.955c-0.242,0-0.487,0.062-0.71,0.194c-0.664,0.391-0.885,1.242-0.499,1.906c0.013,0.021,0.025,0.043,0.038,0.063c0.262,0.436,0.724,0.678,1.197,0.678c0.243,0,0.49-0.063,0.714-0.197c0.665-0.396,0.883-1.257,0.489-1.922l-0.02-0.034C3.526,13.201,3.059,12.955,2.579,12.955L2.579,12.955z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment5 {
|
||||
-fx-shape:"M5.772,16.09c-0.489,0-0.965,0.257-1.223,0.712c-0.38,0.671-0.146,1.52,0.523,1.901c0.002,0.001,0.004,0.002,0.007,0.004h0c0.004,0.002,0.008,0.004,0.012,0.007c0,0,0,0,0.001,0c0.001,0.001,0.002,0.002,0.004,0.002c0.001,0.001,0.003,0.002,0.004,0.003c0,0,0.001,0,0.001,0.001c0.012,0.006,0.023,0.013,0.035,0.019c0.214,0.119,0.446,0.176,0.675,0.176c0.489,0,0.963-0.258,1.22-0.716c0.377-0.675,0.137-1.529-0.537-1.908l-0.035-0.02C6.242,16.149,6.006,16.09,5.772,16.09L5.772,16.09z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment6 {
|
||||
-fx-shape:"M10.14,17.198c-0.006,0-0.013,0-0.02,0h-0.039c-0.773,0.011-1.394,0.646-1.385,1.419c0.008,0.767,0.631,1.382,1.396,1.382c0.003,0,0.006,0,0.009-0.001c0.024,0,0.051,0,0.075-0.001c0.769-0.016,1.38-0.648,1.367-1.418C11.53,17.813,10.904,17.198,10.14,17.198L10.14,17.198z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment7 {
|
||||
-fx-shape:"M14.433,15.97c-0.245,0-0.494,0.064-0.72,0.2l-0.034,0.021c-0.663,0.397-0.88,1.258-0.483,1.922c0.261,0.439,0.725,0.683,1.2,0.683c0.24,0,0.484-0.062,0.707-0.194c0.022-0.013,0.044-0.025,0.065-0.039c0.656-0.399,0.866-1.254,0.469-1.913C15.373,16.212,14.909,15.97,14.433,15.97L14.433,15.97z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment8 {
|
||||
-fx-shape:"M17.539,12.748c-0.493,0-0.973,0.261-1.229,0.723l-0.02,0.034c-0.376,0.676-0.133,1.53,0.542,1.907c0.216,0.121,0.45,0.178,0.681,0.178c0.487,0,0.96-0.256,1.217-0.71c0.003-0.006,0.007-0.012,0.01-0.019c0.007-0.013,0.015-0.025,0.021-0.038l0,0c0.002-0.003,0.003-0.005,0.004-0.008c0.369-0.675,0.124-1.521-0.55-1.893C18.001,12.805,17.769,12.748,17.539,12.748L17.539,12.748z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment9 {
|
||||
-fx-shape:"M18.603,8.408c-0.011,0-0.021,0-0.031,0c-0.773,0.018-1.388,0.657-1.373,1.431l0.001,0.04c0.015,0.765,0.641,1.377,1.403,1.377c0.008,0,0.016,0,0.023,0c0.77-0.013,1.383-0.646,1.373-1.414c0-0.003,0-0.006,0-0.009l0,0c-0.001-0.019-0.001-0.037-0.001-0.055c0-0.001,0-0.001-0.001-0.002c0-0.002,0-0.004,0-0.006C19.979,9.012,19.358,8.408,18.603,8.408L18.603,8.408z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment10 {
|
||||
-fx-shape:"M17.345,4.121c-0.248,0-0.5,0.066-0.728,0.205c-0.659,0.403-0.869,1.266-0.468,1.927l0.021,0.034c0.265,0.435,0.728,0.675,1.202,0.675c0.247,0,0.497-0.065,0.724-0.202c0.659-0.397,0.871-1.252,0.477-1.912c-0.007-0.011-0.013-0.021-0.02-0.032c-0.001-0.002-0.002-0.003-0.002-0.004c-0.001,0-0.001-0.001-0.001-0.002c-0.004-0.005-0.008-0.011-0.011-0.017c0-0.001,0-0.001-0.001-0.001c-0.001-0.002-0.002-0.004-0.004-0.007C18.271,4.358,17.813,4.121,17.345,4.121L17.345,4.121z";
|
||||
}
|
||||
.progress-indicator:indeterminate .segment11 {
|
||||
-fx-shape:"M14.104,1.039c-0.494,0-0.974,0.264-1.227,0.729c-0.37,0.679-0.12,1.531,0.559,1.903l0.034,0.019c0.214,0.117,0.445,0.173,0.673,0.173c0.495,0,0.976-0.262,1.231-0.726c0.371-0.674,0.129-1.519-0.542-1.894c-0.012-0.006-0.024-0.013-0.036-0.02c-0.007-0.004-0.014-0.008-0.021-0.012c-0.003-0.001-0.006-0.003-0.009-0.005C14.556,1.094,14.329,1.039,14.104,1.039L14.104,1.039z";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Text COMMON *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.text-input {
|
||||
-fx-text-fill: -fx-dark-text-color;
|
||||
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
|
||||
-fx-highlight-text-fill: -fx-text-inner-color;
|
||||
-fx-prompt-text-fill: -fx-control-inner-background;
|
||||
-fx-border-color: -fx-text-box-border;
|
||||
-fx-border-width: 1px;
|
||||
-fx-background-color: #FFFFFF;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 0;
|
||||
-fx-cursor: text;
|
||||
-fx-padding: 2px;
|
||||
}
|
||||
.text-input:focused {
|
||||
-fx-highlight-fill: -fx-accent;
|
||||
-fx-border-color: -fx-focus-color;
|
||||
-fx-border-width: 1px;
|
||||
-fx-background-color: -fx-focus-color, #FFFFFF;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-prompt-text-fill: -fx-control-inner-background;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* PopupMenu *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.context-menu {
|
||||
-fx-background-color: derive(-fx-background, -30%), -fx-background;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-padding: 1px;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
|
||||
}
|
||||
.context-menu > .separator {
|
||||
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* MenuItem *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.menu-item {
|
||||
-fx-background-color: transparent;
|
||||
-fx-background-insets:0.0;
|
||||
-fx-padding:0.2em 1em 0.2em 1em;
|
||||
-fx-border-width: 0.0 0.0 0.0 0.0;
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
.menu-item > .left-container {
|
||||
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
|
||||
}
|
||||
.menu-item > .graphic-container {
|
||||
-fx-padding: 0em 0.333em 0em 0em;
|
||||
}
|
||||
.menu-item >.label {
|
||||
-fx-padding: 0em 0.5em 0em 0em;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
}
|
||||
.menu-item:disabled > .label {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
.menu-item:focused {
|
||||
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
|
||||
-fx-background-insets: 0, 1;
|
||||
}
|
||||
.menu-item > .right-container {
|
||||
-fx-padding: 0em 0em 0em 0.5em;
|
||||
}
|
||||
.menu-item:show-mnemonics > .mnemonic-underline {
|
||||
-fx-stroke: -fx-text-fill;
|
||||
}
|
||||
.menu > .right-container > .arrow {
|
||||
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
|
||||
-fx-background-color: -fx-color;
|
||||
-fx-shape: "M0,-4L4,0L0,4Z";
|
||||
-fx-scale-shape: false;
|
||||
}
|
||||
.menu:selected > .right-container > .arrow {
|
||||
-fx-background-color: white;
|
||||
}
|
||||
.menu-item:disabled {
|
||||
-fx-opacity: -fx-disabled-opacity;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ComboBox *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Customie the ListCell that appears in the ComboBox button itself */
|
||||
.combo-box > .list-cell {
|
||||
-fx-background: transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
-fx-padding: 0.1em;
|
||||
}
|
||||
.combo-box-base > .arrow-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 0 0.1em 0 0.1em;
|
||||
}
|
||||
|
||||
.combo-box-popup > .list-view {
|
||||
-fx-background-color: #606060, white;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
|
||||
}
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding:0.1em;
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
|
||||
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
|
||||
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
|
||||
-fx-background-insets: 0, 1;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* SplitPane *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.split-pane > .split-pane-divider {
|
||||
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ListView and ListCell *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.list-view > .virtual-flow > .scroll-bar:vertical{
|
||||
-fx-background-insets: 0, 0 0 0 1;
|
||||
-fx-padding: -1 -1 -1 0;
|
||||
}
|
||||
.list-view > .virtual-flow > .scroll-bar:horizontal{
|
||||
-fx-background-insets: 0, 1 0 0 0;
|
||||
-fx-padding: 0 -1 -1 -1;
|
||||
}
|
||||
.list-view > .virtual-flow > .corner {
|
||||
-fx-background-color: -fx-box-border, -fx-base;
|
||||
-fx-background-insets: 0, 1 0 0 1;
|
||||
}
|
||||
.list-cell {
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
|
||||
-fx-text-fill: -fx-text-inner-color;
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused {
|
||||
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-control-inner-background;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
}
|
||||
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
|
||||
-fx-background-color: #DEDEDE, #F7F7F7;
|
||||
-fx-background-insets: 0, 1;
|
||||
}
|
||||
.list-cell:filled:hover {
|
||||
-fx-background-color: #F7F7F7;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Tooltip *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.tooltip {
|
||||
-fx-background-color: -fx-background;
|
||||
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
|
||||
-fx-font-size: 0.8em;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Charts *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart {
|
||||
-fx-padding: 5px;
|
||||
}
|
||||
.chart-content {
|
||||
-fx-padding: 10px;
|
||||
}
|
||||
.chart-title {
|
||||
-fx-font-size: 1.4em;
|
||||
}
|
||||
.chart-legend {
|
||||
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
|
||||
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
|
||||
-fx-background-insets: 0,1;
|
||||
-fx-background-radius: 6,5;
|
||||
-fx-padding: 6px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Axis *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.axis {
|
||||
AXIS_COLOR: derive(-fx-background,-20%);
|
||||
-fx-tick-label-font-size: 0.833333em; /* 10px */
|
||||
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
|
||||
}
|
||||
.axis:top {
|
||||
-fx-border-color: transparent transparent AXIS_COLOR transparent;
|
||||
}
|
||||
.axis:right {
|
||||
-fx-border-color: transparent transparent transparent AXIS_COLOR;
|
||||
}
|
||||
.axis:bottom {
|
||||
-fx-border-color: AXIS_COLOR transparent transparent transparent;
|
||||
}
|
||||
.axis:left {
|
||||
-fx-border-color: transparent AXIS_COLOR transparent transparent;
|
||||
}
|
||||
.axis-tick-mark,
|
||||
.axis-minor-tick-mark {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: AXIS_COLOR;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* ChartPlot *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart-vertical-grid-lines {
|
||||
-fx-stroke: derive(-fx-background,-10%);
|
||||
-fx-stroke-dash-array: 0.25em, 0.25em;
|
||||
}
|
||||
.chart-horizontal-grid-lines {
|
||||
-fx-stroke: derive(-fx-background,-10%);
|
||||
}
|
||||
.chart-alternative-column-fill {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: null;
|
||||
}
|
||||
.chart-alternative-row-fill {
|
||||
-fx-fill: null;
|
||||
-fx-stroke: null;
|
||||
}
|
||||
.chart-vertical-zero-line,
|
||||
.chart-horizontal-zero-line {
|
||||
-fx-stroke: derive(-fx-text-background-color, 40%);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* LineChart *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.chart-line-symbol {
|
||||
-fx-background-color: #f9d900, white;
|
||||
-fx-background-insets: 0, 2;
|
||||
-fx-background-radius: 5px;
|
||||
-fx-padding: 5px;
|
||||
}
|
||||
.chart-series-line {
|
||||
-fx-stroke: #f9d900;
|
||||
-fx-stroke-width: 3px;
|
||||
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
|
||||
}
|
||||
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
|
||||
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
|
||||
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
|
||||
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
|
||||
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
|
||||
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
|
||||
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
|
||||
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
|
||||
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
|
||||
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
|
||||
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
|
||||
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
|
||||
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
|
||||
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
|
||||
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
|
||||
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Combinations *
|
||||
* *
|
||||
* This section is for special handling of when one control is nested inside *
|
||||
* another control. There are many cases where we would end up with ugly *
|
||||
* double borders that are fixed here. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.split-pane > * > .table-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .list-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .tree-view { -fx-padding: 0px; }
|
||||
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
|
||||
.split-pane > * > .split-pane {
|
||||
-fx-background-insets: 0, 0;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* ############################################################################
|
||||
# Workaround for RT-27627 #
|
||||
############################################################################ */
|
||||
|
||||
.choice-box > .open-button > .arrow { doh: true; }
|
||||
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
|
||||
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
|
||||
.tree-table-view { doh: true; }
|
||||
.tree-table-view:focused { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
|
||||
.tree-table-view > .virtual-flow > .corner { doh: true; }
|
||||
.tree-table-row-cell:focused { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
|
||||
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
|
||||
.tree-table-row-cell:filled:selected:focused { doh: true; }
|
||||
.tree-table-row-cell:filled:selected { doh: true; }
|
||||
.tree-table-row-cell:selected:disabled { doh: true; }
|
||||
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
|
||||
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
|
||||
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
|
||||
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
|
||||
.tree-table-view:constrained-resize .filler { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
|
||||
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
|
||||
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
|
||||
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
|
||||
.tree-table-cell:selected:disabled { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
|
||||
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
|
||||
.tree-table-view .column-resize-line { doh: true; }
|
||||
.tree-table-view > .column-header-background { doh: true; }
|
||||
.tree-table-view .column-header { doh: true; }
|
||||
.tree-table-view .filler { doh: true; }
|
||||
.tree-table-view .column-header .sort-order{ doh: true; }
|
||||
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
|
||||
.tree-table-view .show-hide-column-image { doh: true; }
|
||||
.tree-table-view .column-drag-header { doh: true; }
|
||||
.tree-table-view .column-overlay { doh: true; }
|
||||
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
|
||||
.tree-table-view .empty-table { doh: true; }
|
||||
.axis-minor-tick-mark { doh: true; }
|
||||
.chart-horizontal-zero-line { doh: true; }
|
||||
.stacked-bar-chart:horizontal .chart-bar { doh: true; }
|
||||
55
main/ui/src/main/resources/fxml/initialize.fxml
Normal file
55
main/ui/src/main/resources/fxml/initialize.fxml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import org.cryptomator.ui.controls.*?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import org.cryptomator.ui.controls.SecPasswordField?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
|
||||
<padding>
|
||||
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
|
||||
</padding>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="38.2"/>
|
||||
<ColumnConstraints percentWidth="61.8"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.username" />
|
||||
<TextField fx:id="usernameField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
53
main/ui/src/main/resources/fxml/main.fxml
Normal file
53
main/ui/src/main/resources/fxml/main.fxml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.URL?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.Pane?>
|
||||
<?import javafx.scene.control.ToolBar?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ContextMenu?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
|
||||
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
|
||||
|
||||
<fx:define>
|
||||
<fx:include fx:id="welcomeView" source="welcome.fxml" />
|
||||
<ContextMenu fx:id="directoryContextMenu">
|
||||
<items>
|
||||
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
|
||||
<!-- TODO: -->
|
||||
<MenuItem text="%main.directoryList.contextMenu.addUser" disable="true" />
|
||||
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
|
||||
</items>
|
||||
</ContextMenu>
|
||||
</fx:define>
|
||||
|
||||
<children>
|
||||
<VBox prefWidth="200.0">
|
||||
<children>
|
||||
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
|
||||
<ToolBar VBox.vgrow="NEVER">
|
||||
<items>
|
||||
<Button text="+" onAction="#didClickAddDirectory" />
|
||||
</items>
|
||||
</ToolBar>
|
||||
</children>
|
||||
</VBox>
|
||||
<Pane fx:id="contentPane">
|
||||
<children>
|
||||
<fx:reference source="welcomeView" />
|
||||
</children>
|
||||
</Pane>
|
||||
</children>
|
||||
|
||||
</HBox>
|
||||
|
||||
51
main/ui/src/main/resources/fxml/unlock.fxml
Normal file
51
main/ui/src/main/resources/fxml/unlock.fxml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import org.cryptomator.ui.controls.SecPasswordField?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ProgressIndicator?>
|
||||
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
|
||||
<padding>
|
||||
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
|
||||
</padding>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="38.2"/>
|
||||
<ColumnConstraints percentWidth="61.8"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label text="%unlock.label.username" GridPane.rowIndex="0" GridPane.columnIndex="0" />
|
||||
<ComboBox fx:id="usernameBox" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" promptText="$access.label.username" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label text="%unlock.label.password" GridPane.rowIndex="1" GridPane.columnIndex="0" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
|
||||
|
||||
<!-- Row 3 -->
|
||||
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
46
main/ui/src/main/resources/fxml/unlocked.fxml
Normal file
46
main/ui/src/main/resources/fxml/unlocked.fxml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.chart.LineChart?>
|
||||
<?import javafx.scene.chart.NumberAxis?>
|
||||
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
|
||||
<padding>
|
||||
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
|
||||
</padding>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="38.2"/>
|
||||
<ColumnConstraints percentWidth="61.8"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
|
||||
|
||||
<!-- Row 2 -->
|
||||
<LineChart fx:id="ioGraph" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="300.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
|
||||
<xAxis><NumberAxis fx:id="xAxis" forceZeroInRange="false" tickMarkVisible="false" minorTickVisible="false" tickLabelsVisible="false" autoRanging="false"/></xAxis>
|
||||
<yAxis><NumberAxis label="%unlocked.ioGraph.yAxis.label" autoRanging="true" forceZeroInRange="true" /></yAxis>
|
||||
</LineChart>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
33
main/ui/src/main/resources/fxml/welcome.fxml
Normal file
33
main/ui/src/main/resources/fxml/welcome.fxml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.scene.shape.Arc?>
|
||||
<?import javafx.scene.shape.QuadCurve?>
|
||||
<?import javafx.scene.shape.Path?>
|
||||
<?import javafx.scene.shape.Line?>
|
||||
|
||||
|
||||
<AnchorPane xmlns:fx="http://javafx.com/fxml">
|
||||
|
||||
<children>
|
||||
<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
|
||||
<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
|
||||
|
||||
<QuadCurve AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
|
||||
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
|
||||
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
|
||||
</children>
|
||||
|
||||
</AnchorPane>
|
||||
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import org.cryptomator.ui.controls.*?>
|
||||
|
||||
|
||||
<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
|
||||
<stylesheets>
|
||||
<URL value="@panels.css" />
|
||||
</stylesheets>
|
||||
|
||||
<padding>
|
||||
<Insets top="10" right="10" bottom="10" left="10" />
|
||||
</padding>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
|
||||
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
|
||||
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
|
||||
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.username" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="usernameField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.password" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%initialize.label.retypePassword" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="4" GridPane.columnIndex="1" defaultButton="true" onAction="#initWorkDir" disable="true" focusTraversable="false"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
@@ -6,29 +6,48 @@
|
||||
# Contributors:
|
||||
# Sebastian Stenzel - initial API and implementation
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
app.name=Cryptomator
|
||||
|
||||
# main.fxml
|
||||
toolbarbutton.initialize=Initialize Vault
|
||||
toolbarbutton.access=Access Vault
|
||||
main.directoryList.contextMenu.remove=Remove from list
|
||||
main.directoryList.contextMenu.addUser=Add user
|
||||
main.directoryList.contextMenu.changePassword=Change password
|
||||
|
||||
|
||||
# welcome.fxml
|
||||
welcome.welcomeLabel=Welcome to Cryptomator
|
||||
welcome.addButtonInstructionLabel=Start by adding a new vault :-)
|
||||
|
||||
|
||||
# initialize.fxml
|
||||
initialize.label.workDir=New vault location
|
||||
initialize.button.chooseWorkDir=Choose...
|
||||
initialize.label.username=Username
|
||||
initialize.label.password=Password
|
||||
initialize.label.retypePassword=Retype
|
||||
initialize.button.initWorkDir=Initialize Vault
|
||||
initialize.messageLabel.alreadyInitialized=Vault in this location already exists.
|
||||
initialize.messageLabel.invalidPath=Invalid vault location.
|
||||
initialize.label.retypePassword=Retype password
|
||||
initialize.button.ok=Create vault
|
||||
initialize.alert.directoryIsNotEmpty.title=The chosen directory is not empty
|
||||
initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
|
||||
|
||||
# access.fxml
|
||||
access.label.workDir=Vault location
|
||||
access.label.username=Username
|
||||
access.label.password=Password
|
||||
access.button.chooseWorkDir=Choose...
|
||||
access.button.startServer=Start Server
|
||||
access.button.stopServer=Stop Server
|
||||
access.messageLabel.wrongPassword=Wrong password.
|
||||
access.messageLabel.invalidStorageLocation=Vault directory invalid.
|
||||
access.messageLabel.decryptionFailed=Decryption failed.
|
||||
access.messageLabel.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
|
||||
access.messageLabel.mountFailed=Mounting WebDAV share (Port %d) failed.
|
||||
|
||||
# unlock.fxml
|
||||
unlock.label.username=Username
|
||||
unlock.label.password=Password
|
||||
unlock.button.unlock=Unlock vault
|
||||
unlock.errorMessage.wrongPassword=Wrong password.
|
||||
unlock.errorMessage.decryptionFailed=Decryption failed.
|
||||
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
|
||||
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
|
||||
|
||||
|
||||
# unlocked.fxml
|
||||
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
|
||||
unlocked.button.lock=Lock vault
|
||||
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
|
||||
|
||||
|
||||
# tray icon
|
||||
tray.menu.open=Open
|
||||
tray.menu.quit=Quit
|
||||
tray.infoMsg.title=Still running
|
||||
tray.infoMsg.msg=Cryptomator is still alive. Quit it from the tray icon.
|
||||
tray.infoMsg.msg.osx=Cryptomator is still alive. Quit it from the menu bar icon.
|
||||
|
||||
33
main/ui/src/main/resources/log4j2.xml
Normal file
33
main/ui/src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
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 - switched to log4j 2
|
||||
-->
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- show our own debug messages: -->
|
||||
<Logger name="org.cryptomator" level="DEBUG"/>
|
||||
<!-- mute dependencies: -->
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -1,32 +0,0 @@
|
||||
@CHARSET "US-ASCII";
|
||||
|
||||
.text {
|
||||
-fx-font-smoothing-type: lcd;
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
|
||||
-fx-padding: 5.0 10.0 5.0 10.0;
|
||||
-fx-border-color: #888888;
|
||||
-fx-border-width: 1.0 0.0 1.0 0.0;
|
||||
-fx-border-insets: 0.0;
|
||||
-fx-alignment: CENTER;
|
||||
}
|
||||
|
||||
.tool-bar .toggle-button {
|
||||
-fx-text-fill: #FFFFFF;
|
||||
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
|
||||
-fx-border-color: #888888;
|
||||
-fx-background-insets: 0.0, 1.0;
|
||||
-fx-background-radius: 4.0, 4.0;
|
||||
-fx-border-radius: 3.0;
|
||||
-fx-border-width: 0.5;
|
||||
-fx-font-family: "lucida-grande";
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.tool-bar .toggle-button:armed,
|
||||
.tool-bar .toggle-button:selected {
|
||||
-fx-background-color: linear-gradient(to bottom, #444444, #555555 30%, #000000);
|
||||
-fx-border-color: #FFFFFF;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<?import java.net.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
|
||||
<VBox fx:id="rootVBox" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
|
||||
<stylesheets>
|
||||
<URL value="@main.css" />
|
||||
</stylesheets>
|
||||
|
||||
<fx:define>
|
||||
<fx:include fx:id="initializePanel" source="initialize.fxml" />
|
||||
<fx:include fx:id="accessPanel" source="access.fxml" />
|
||||
</fx:define>
|
||||
|
||||
<children>
|
||||
<ToolBar>
|
||||
<items>
|
||||
<fx:define>
|
||||
<ToggleGroup fx:id="toolbarButtonGroup" />
|
||||
</fx:define>
|
||||
<ToggleButton text="%toolbarbutton.initialize" toggleGroup="$toolbarButtonGroup" onAction="#showInitializePane" />
|
||||
<ToggleButton text="%toolbarbutton.access" toggleGroup="$toolbarButtonGroup" onAction="#showAccessPane" selected="true" />
|
||||
</items>
|
||||
</ToolBar>
|
||||
<fx:reference source="accessPanel"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
@CHARSET "US-ASCII";
|
||||
|
||||
.root {
|
||||
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
|
||||
}
|
||||
|
||||
.text {
|
||||
-fx-font-smoothing-type: lcd;
|
||||
}
|
||||
|
||||
.label {
|
||||
-fx-alignment: CENTER;
|
||||
-fx-font-family: "lucida-grande";
|
||||
}
|
||||
|
||||
.button,
|
||||
.combo-box {
|
||||
-fx-text-fill: #000000;
|
||||
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
|
||||
-fx-border-color: #888888;
|
||||
-fx-background-insets: 0.0, 1.0;
|
||||
-fx-background-radius: 4.0, 4.0;
|
||||
-fx-border-radius: 3.0;
|
||||
-fx-border-width: 0.5;
|
||||
-fx-font-family: "lucida-grande";
|
||||
-fx-font-weight: normal;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
-fx-border-radius: 3.0;
|
||||
-fx-border-width: 0.5;
|
||||
-fx-border-color: #888888;
|
||||
-fx-focus-color: #FF0000;
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 5 2 5 2;
|
||||
}
|
||||
|
||||
.text-field:focused {
|
||||
-fx-background-color: #DDDDDD;
|
||||
}
|
||||
|
||||
.button:armed,
|
||||
.button:selected,
|
||||
.combo-box:armed,
|
||||
.combo-box:selected {
|
||||
-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
|
||||
}
|
||||
|
||||
.combo-box .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
}
|
||||
|
||||
.combo-box .list-cell:hover {
|
||||
-fx-background-color: #DDDDDD;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view {
|
||||
-fx-padding: 0 0 0 0;
|
||||
-fx-background-insets: 0, 0;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0);
|
||||
}
|
||||
BIN
main/ui/src/main/resources/tray_icon.png
Normal file
BIN
main/ui/src/main/resources/tray_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Reference in New Issue
Block a user