mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-23 02:56:59 -04:00
separated filename shortening layer from metadata hiding layer
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class LazyInitializer {
|
||||
|
||||
private LazyInitializer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
||||
*
|
||||
* @param <T> Type of the value
|
||||
* @param reference A reference to a maybe not yet initialized value.
|
||||
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
|
||||
* @return The initialized value
|
||||
*/
|
||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||
final T existingInstance = reference.get();
|
||||
if (existingInstance != null) {
|
||||
return existingInstance;
|
||||
} else {
|
||||
final T newInstance = factory.get();
|
||||
if (reference.compareAndSet(null, newInstance)) {
|
||||
return newInstance;
|
||||
} else {
|
||||
return reference.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,13 +14,13 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
@@ -67,7 +67,7 @@ public class CryptorImpl implements Cryptor {
|
||||
@Override
|
||||
public FilenameCryptor getFilenameCryptor() {
|
||||
assertKeysExist();
|
||||
return initializeLazily(filenameCryptor, () -> {
|
||||
return LazyInitializer.initializeLazily(filenameCryptor, () -> {
|
||||
return new FilenameCryptorImpl(encryptionKey, macKey);
|
||||
});
|
||||
}
|
||||
@@ -75,28 +75,11 @@ public class CryptorImpl implements Cryptor {
|
||||
@Override
|
||||
public FileContentCryptor getFileContentCryptor() {
|
||||
assertKeysExist();
|
||||
return initializeLazily(fileContentCryptor, () -> {
|
||||
return LazyInitializer.initializeLazily(fileContentCryptor, () -> {
|
||||
return new FileContentCryptorImpl(encryptionKey, macKey, randomSource);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
||||
*/
|
||||
private <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||
final T existingInstance = reference.get();
|
||||
if (existingInstance != null) {
|
||||
return existingInstance;
|
||||
} else {
|
||||
final T newInstance = factory.get();
|
||||
if (reference.compareAndSet(null, newInstance)) {
|
||||
return newInstance;
|
||||
} else {
|
||||
return reference.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertKeysExist() {
|
||||
if (encryptionKey == null || encryptionKey.isDestroyed()) {
|
||||
throw new IllegalStateException("No or invalid encryptionKey.");
|
||||
|
||||
@@ -4,6 +4,7 @@ import static org.cryptomator.filesystem.FileSystemVisitor.fileSystemVisitor;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.impl.TestCryptorImplFactory;
|
||||
@@ -13,20 +14,23 @@ import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.filesystem.blacklisting.BlacklistingFileSystem;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.cryptomator.shortening.ShorteningFileSystem;
|
||||
import org.cryptomator.filesystem.shortening.ShorteningFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class EncryptAndShortenIntegrationTest {
|
||||
|
||||
// private static final Logger LOG =
|
||||
// LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
|
||||
// private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfLongFolderNames() {
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final FileSystem shorteningFs = new ShorteningFileSystem(physicalFs, physicalFs.folder("m"), 70);
|
||||
final Predicate<Node> isMetadataFolder = (Node node) -> node.equals(physicalFs.folder("m"));
|
||||
final FileSystem metadataHidingFs = new BlacklistingFileSystem(physicalFs, isMetadataFolder);
|
||||
final FileSystem shorteningFs = new ShorteningFileSystem(metadataHidingFs, physicalFs.folder("m"), 70);
|
||||
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.randomizeMasterkey();
|
||||
final FileSystem fs = new CryptoFileSystem(shorteningFs, cryptor, "foo");
|
||||
@@ -36,24 +40,27 @@ public class EncryptAndShortenIntegrationTest {
|
||||
final Folder longFolder = fs.folder("this will be a long filename after encryption");
|
||||
longFolder.create();
|
||||
|
||||
// on the first (physical) layer all files including metadata files are visible:
|
||||
// the long name will produce a metadata file on the physical layer:
|
||||
// LOG.debug("Physical file system:\n" +
|
||||
// DirectoryPrinter.print(physicalFs));
|
||||
// LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs));
|
||||
Assert.assertEquals(1, physicalFs.folder("m").folders().count());
|
||||
Assert.assertTrue(physicalFs.folder("m").exists());
|
||||
|
||||
// on the second layer all .lng files are resolved to their actual
|
||||
// names:
|
||||
// LOG.debug("Unlimited filename length:\n" +
|
||||
// DirectoryPrinter.print(shorteningFs));
|
||||
// on the second (blacklisting) layer we hide the metadata folder:
|
||||
// LOG.debug("Filtered files:\n" + DirectoryPrinter.print(metadataHidingFs));
|
||||
Assert.assertEquals(1, metadataHidingFs.folders().count()); // only "d", no "m".
|
||||
|
||||
// on the third layer all .lng files are resolved to their actual names:
|
||||
// LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs));
|
||||
fileSystemVisitor() //
|
||||
.forEachNode(node -> {
|
||||
Assert.assertFalse(node.name().endsWith(".lng"));
|
||||
}) //
|
||||
.visit(shorteningFs);
|
||||
// on the third (cleartext layer) we have cleartext names on the root
|
||||
// level:
|
||||
|
||||
// on the fourth (cleartext) layer we have cleartext names on the root level:
|
||||
// LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs));
|
||||
Assert.assertArrayEquals(new String[] { "normal folder name", "this will be a long filename after encryption" }, fs.folders().map(Node::name).sorted().toArray());
|
||||
Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -58,7 +58,11 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer target) {
|
||||
return ByteBuffers.copy(content, target);
|
||||
if (content.hasRemaining()) {
|
||||
return ByteBuffers.copy(content, target);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,7 +118,7 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
|
||||
// returning null removes the entry.
|
||||
return null;
|
||||
});
|
||||
assert !this.exists();
|
||||
assert!this.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.cryptomator.filesystem.blacklisting;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingReadableFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingWritableFile;
|
||||
|
||||
class BlacklistingFile extends DelegatingFile<DelegatingReadableFile, DelegatingWritableFile, BlacklistingFolder> {
|
||||
|
||||
public BlacklistingFile(BlacklistingFolder parent, File delegate) {
|
||||
super(parent, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelegatingReadableFile openReadable() throws UncheckedIOException {
|
||||
return new DelegatingReadableFile(delegate.openReadable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelegatingWritableFile openWritable() throws UncheckedIOException {
|
||||
return new DelegatingWritableFile(delegate.openWritable());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.cryptomator.filesystem.blacklisting;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
public class BlacklistingFileSystem extends BlacklistingFolder implements FileSystem {
|
||||
|
||||
public BlacklistingFileSystem(Folder root, Predicate<Node> hiddenNodes) {
|
||||
super(null, root, hiddenNodes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.cryptomator.filesystem.blacklisting;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFolder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingReadableFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingWritableFile;
|
||||
|
||||
class BlacklistingFolder extends DelegatingFolder<DelegatingReadableFile, DelegatingWritableFile, BlacklistingFolder, BlacklistingFile> {
|
||||
|
||||
private final Predicate<Node> hiddenNodes;
|
||||
|
||||
public BlacklistingFolder(BlacklistingFolder parent, Folder delegate, Predicate<Node> hiddenNodes) {
|
||||
super(parent, delegate);
|
||||
this.hiddenNodes = hiddenNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() {
|
||||
return Stream.concat(folders(), files());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<BlacklistingFolder> folders() {
|
||||
return delegate.folders().filter(hiddenNodes.negate()).map(this::folder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<BlacklistingFile> files() {
|
||||
return delegate.files().filter(hiddenNodes.negate()).map(this::file);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlacklistingFile file(File delegate) {
|
||||
if (hiddenNodes.test(delegate)) {
|
||||
throw new UncheckedIOException("'" + delegate.name() + "' is a reserved name.", new FileAlreadyExistsException(delegate.name()));
|
||||
}
|
||||
return new BlacklistingFile(this, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlacklistingFolder folder(Folder delegate) {
|
||||
if (hiddenNodes.test(delegate)) {
|
||||
throw new UncheckedIOException("'" + delegate.name() + "' is a reserved name.", new FileAlreadyExistsException(delegate.name()));
|
||||
}
|
||||
return new BlacklistingFolder(this, delegate, hiddenNodes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package org.cryptomator.shortening;
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.Writer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
class FilenameShortener {
|
||||
|
||||
@@ -53,8 +55,10 @@ class FilenameShortener {
|
||||
final File mappingFile = mappingFile(shortName);
|
||||
if (!mappingFile.exists()) {
|
||||
mappingFile.parent().get().create();
|
||||
try (WritableFile writable = mappingFile.openWritable()) {
|
||||
writable.write(ByteBuffer.wrap(longName.getBytes(StandardCharsets.UTF_8)));
|
||||
try (Writer writer = Channels.newWriter(mappingFile.openWritable(), StandardCharsets.UTF_8.newEncoder(), -1)) {
|
||||
writer.write(longName);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,14 +73,10 @@ class FilenameShortener {
|
||||
if (!mappingFile.exists()) {
|
||||
throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
|
||||
} else {
|
||||
try (ReadableFile readable = mappingFile.openReadable()) {
|
||||
// TODO buffer might be to small
|
||||
final ByteBuffer buf = ByteBuffer.allocate(1024);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
final byte[] bytes = new byte[buf.remaining()];
|
||||
buf.get(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
try (Reader reader = Channels.newReader(mappingFile.openReadable(), StandardCharsets.UTF_8.newDecoder(), -1)) {
|
||||
return IOUtils.toString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingReadableFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingWritableFile;
|
||||
|
||||
class ShorteningFile extends DelegatingFile<DelegatingReadableFile, DelegatingWritableFile, ShorteningFolder> {
|
||||
|
||||
private final AtomicReference<String> longName;
|
||||
private final FilenameShortener shortener;
|
||||
|
||||
public ShorteningFile(ShorteningFolder parent, File delegate, String name, FilenameShortener shortener) {
|
||||
super(parent, delegate);
|
||||
this.longName = new AtomicReference<>(name);
|
||||
this.shortener = shortener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return LazyInitializer.initializeLazily(longName, () -> {
|
||||
return shortener.inflate(shortenedName());
|
||||
});
|
||||
}
|
||||
|
||||
private String shortenedName() {
|
||||
return delegate.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelegatingReadableFile openReadable() throws UncheckedIOException {
|
||||
return new DelegatingReadableFile(delegate.openReadable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelegatingWritableFile openWritable() throws UncheckedIOException {
|
||||
if (shortener.isShortened(shortenedName())) {
|
||||
shortener.saveMapping(name(), shortenedName());
|
||||
}
|
||||
return new DelegatingWritableFile(delegate.openWritable());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
public class ShorteningFileSystem extends ShorteningFolder implements FileSystem {
|
||||
|
||||
public ShorteningFileSystem(Folder root, Folder metadataRoot, int threshold) {
|
||||
super(null, root, "", new FilenameShortener(metadataRoot, threshold));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFolder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingReadableFile;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingWritableFile;
|
||||
|
||||
class ShorteningFolder extends DelegatingFolder<DelegatingReadableFile, DelegatingWritableFile, ShorteningFolder, ShorteningFile> {
|
||||
|
||||
private final AtomicReference<String> longName;
|
||||
private final FilenameShortener shortener;
|
||||
|
||||
public ShorteningFolder(ShorteningFolder parent, Folder delegate, String name, FilenameShortener shortener) {
|
||||
super(parent, delegate);
|
||||
this.longName = new AtomicReference<>(name);
|
||||
this.shortener = shortener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return LazyInitializer.initializeLazily(longName, () -> {
|
||||
return shortener.inflate(shortenedName());
|
||||
});
|
||||
}
|
||||
|
||||
private String shortenedName() {
|
||||
return delegate.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShorteningFile file(String name) throws UncheckedIOException {
|
||||
return new ShorteningFile(this, delegate.file(shortener.deflate(name)), name, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShorteningFolder folder(String name) throws UncheckedIOException {
|
||||
return new ShorteningFolder(this, delegate.folder(shortener.deflate(name)), name, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ShorteningFile file(File delegate) {
|
||||
return new ShorteningFile(this, delegate, null, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ShorteningFolder folder(Folder delegate) {
|
||||
return new ShorteningFolder(this, delegate, null, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create() throws UncheckedIOException {
|
||||
if (exists()) {
|
||||
return;
|
||||
}
|
||||
parent().get().create();
|
||||
if (shortener.isShortened(shortenedName())) {
|
||||
shortener.saveMapping(name(), shortenedName());
|
||||
}
|
||||
super.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Folder destination) {
|
||||
super.moveTo(destination);
|
||||
if (destination instanceof ShorteningFolder) {
|
||||
ShorteningFolder dest = (ShorteningFolder) destination;
|
||||
if (shortener.isShortened(dest.shortenedName())) {
|
||||
shortener.saveMapping(dest.name(), dest.shortenedName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
* {@link org.cryptomator.filesystem.File File} and {@link org.cryptomator.filesystem.Folder Folder} names exceeding a certain length limit will be mapped to shorter equivalents.
|
||||
* The mapping itself is stored in metadata files inside the <code>m/</code> directory on root level.
|
||||
*/
|
||||
package org.cryptomator.shortening;
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.cryptomator.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
class ShorteningFile extends ShorteningNode<File> implements File {
|
||||
|
||||
private final FilenameShortener shortener;
|
||||
|
||||
public ShorteningFile(ShorteningFolder parent, File delegate, String longName, FilenameShortener shortener) {
|
||||
super(parent, delegate, longName);
|
||||
this.shortener = shortener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
return delegate.openReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableFile openWritable() throws UncheckedIOException {
|
||||
if (shortener.isShortened(shortName())) {
|
||||
shortener.saveMapping(name(), shortName());
|
||||
}
|
||||
return delegate.openWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent + name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(File o) {
|
||||
return toString().compareTo(o.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.cryptomator.shortening;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
/**
|
||||
* Filesystem implementation, that shortens filenames when they reach a certain
|
||||
* threshold (inclusive). Shortening is done by SHA1-hashing those files, so a
|
||||
* threshold below the length of the hashed files makes no sense. Hashes are
|
||||
* then mapped back to the original filenames by storing metadata files inside
|
||||
* the given metadataRoot.
|
||||
*/
|
||||
public class ShorteningFileSystem extends ShorteningFolder implements FileSystem {
|
||||
|
||||
public ShorteningFileSystem(Folder root, Folder metadataRoot, int threshold) {
|
||||
super(null, root, "", metadataRoot, new FilenameShortener(metadataRoot, threshold));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
// no-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.cryptomator.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
class ShorteningFolder extends ShorteningNode<Folder> implements Folder {
|
||||
|
||||
private final Folder metadataRoot;
|
||||
private final FilenameShortener shortener;
|
||||
|
||||
public ShorteningFolder(ShorteningFolder parent, Folder delegate, String longName, Folder metadataRoot, FilenameShortener shortener) {
|
||||
super(parent, delegate, longName);
|
||||
this.metadataRoot = metadataRoot;
|
||||
this.shortener = shortener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() {
|
||||
return Stream.concat(this.files(), this.folders());
|
||||
}
|
||||
|
||||
private ShorteningFile existingFile(File original) {
|
||||
final String longName = shortener.inflate(original.name());
|
||||
return new ShorteningFile(this, original, longName, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File file(String name) {
|
||||
final File original = delegate.file(shortener.deflate(name));
|
||||
if (metadataRoot.equals(original)) { // comparing apples and oranges,
|
||||
// but we don't know if the
|
||||
// underlying fs distinguishes
|
||||
// files and folders...
|
||||
throw new UncheckedIOException("'" + name + "' is a reserved name.", new FileAlreadyExistsException(name));
|
||||
}
|
||||
return new ShorteningFile(this, original, name, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends File> files() throws UncheckedIOException {
|
||||
return delegate.files().map(this::existingFile);
|
||||
}
|
||||
|
||||
private ShorteningFolder existingFolder(Folder original) {
|
||||
final String longName = shortener.inflate(original.name());
|
||||
return new ShorteningFolder(this, original, longName, metadataRoot, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Folder folder(String name) {
|
||||
final Folder original = delegate.folder(shortener.deflate(name));
|
||||
if (metadataRoot.equals(original)) {
|
||||
throw new UncheckedIOException("'" + name + "' is a reserved name.", new FileAlreadyExistsException(name));
|
||||
}
|
||||
return new ShorteningFolder(this, original, name, metadataRoot, shortener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Folder> folders() {
|
||||
// if metadataRoot is inside our filesystem, we must filter it out:
|
||||
final Predicate<Node> equalsMetadataRoot = (Node node) -> metadataRoot.equals(node);
|
||||
return delegate.folders().filter(equalsMetadataRoot.negate()).map(this::existingFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create() {
|
||||
if (exists()) {
|
||||
return;
|
||||
}
|
||||
parent().get().create();
|
||||
if (shortener.isShortened(shortName())) {
|
||||
shortener.saveMapping(name(), shortName());
|
||||
}
|
||||
delegate.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
delegate.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Folder target) {
|
||||
if (target instanceof ShorteningFolder) {
|
||||
moveToInternal((ShorteningFolder) target);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Can not move ShorteningFolder to conventional folder.");
|
||||
}
|
||||
}
|
||||
|
||||
private void moveToInternal(ShorteningFolder target) {
|
||||
if (this.isAncestorOf(target) || target.isAncestorOf(this)) {
|
||||
throw new IllegalArgumentException("Can not move directories containing one another (src: " + this + ", dst: " + target + ")");
|
||||
}
|
||||
|
||||
target.create();
|
||||
|
||||
delegate.moveTo(target.delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent + name() + "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package org.cryptomator.shortening;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
class ShorteningNode<E extends Node> implements Node {
|
||||
|
||||
protected final E delegate;
|
||||
protected final ShorteningFolder parent;
|
||||
private final String longName;
|
||||
private final String shortName;
|
||||
|
||||
public ShorteningNode(ShorteningFolder parent, E delegate, String longName) {
|
||||
this.delegate = delegate;
|
||||
this.parent = parent;
|
||||
this.shortName = delegate.name();
|
||||
this.longName = longName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return longName;
|
||||
}
|
||||
|
||||
protected String shortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<? extends Folder> parent() {
|
||||
return Optional.ofNullable(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return delegate.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() {
|
||||
return delegate.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((longName == null) ? 0 : longName.hashCode());
|
||||
result = prime * result + ((parent == null) ? 0 : parent.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ShorteningNode) {
|
||||
ShorteningNode<?> other = (ShorteningNode<?>) obj;
|
||||
return this.getClass() == other.getClass() //
|
||||
&& (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
|
||||
&& (this.longName == null && other.longName == null || this.longName.equals(other.longName));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.cryptomator.filesystem.blacklisting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class BlacklistingFileSystemTest {
|
||||
|
||||
@Test(expected = UncheckedIOException.class)
|
||||
public void testPreventCreationOfMetadataFolder() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder metadataRoot = underlyingFs.folder("m");
|
||||
final Predicate<Node> metadataHidden = (Node n) -> n.equals(metadataRoot);
|
||||
final FileSystem fs = new BlacklistingFileSystem(underlyingFs, metadataHidden);
|
||||
fs.folder("m");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlacklistingOfFilesAndFolders() throws IOException {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder hiddenFolder = underlyingFs.folder("asd");
|
||||
final File hiddenFile = underlyingFs.file("qwe");
|
||||
final Folder visibleFolder = underlyingFs.folder("sdf");
|
||||
final File visibleFile = underlyingFs.file("wer");
|
||||
final Predicate<Node> metadataHidden = (Node n) -> n.equals(hiddenFolder) || n.equals(hiddenFile);
|
||||
final FileSystem fs = new BlacklistingFileSystem(underlyingFs, metadataHidden);
|
||||
hiddenFolder.create();
|
||||
try (WritableByteChannel writable = hiddenFile.openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
visibleFolder.create();
|
||||
try (WritableByteChannel writable = visibleFile.openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
|
||||
Assert.assertArrayEquals(new String[] {"sdf"}, fs.folders().map(Node::name).collect(Collectors.toList()).toArray());
|
||||
Assert.assertArrayEquals(new String[] {"wer"}, fs.files().map(Node::name).collect(Collectors.toList()).toArray());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.cryptomator.shortening;
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -17,21 +17,12 @@ import org.junit.Test;
|
||||
public class ShorteningFileSystemTest {
|
||||
|
||||
@Test
|
||||
public void testCreationOfInvisibleMetadataFolder() {
|
||||
public void testImplicitCreationOfMetadataFolder() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder metadataRoot = underlyingFs.folder("m");
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
|
||||
fs.folder("morethantenchars").create();
|
||||
Assert.assertTrue(metadataRoot.exists());
|
||||
Assert.assertEquals(1, fs.folders().count());
|
||||
}
|
||||
|
||||
@Test(expected = UncheckedIOException.class)
|
||||
public void testPreventCreationOfMetadataFolder() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder metadataRoot = underlyingFs.folder("m");
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
|
||||
fs.folder("m");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -9,6 +9,7 @@ import static org.mockito.Mockito.when;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -82,6 +83,7 @@ public class WeakValuedCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
|
||||
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user