package peergos.shared.user.fs;
import jsinterop.annotations.*;
import peergos.shared.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.asymmetric.*;
import peergos.shared.crypto.random.*;
import peergos.shared.crypto.symmetric.*;
import peergos.shared.user.*;
import peergos.shared.util.*;
import java.io.*;
import java.time.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.*;
import java.util.stream.*;
public class FileTreeNode {
final static int[] BMP = new int[]{66, 77};
final static int[] GIF = new int[]{71, 73, 70};
final static int[] JPEG = new int[]{255, 216};
final static int[] PNG = new int[]{137, 80, 78, 71, 13, 10, 26, 10};
final static int HEADER_BYTES_TO_IDENTIFY_IMAGE_FILE = 8;
final static int THUMBNAIL_SIZE = 100;
final NativeJSThumbnail thumbnail;
RetrievedFilePointer pointer;
private FileProperties props;
String ownername;
Set<String> readers;
Set<String> writers;
Optional<SecretSigningKey> entryWriterKey;
private final Optional<TrieNode> globalRoot;
/**
*
* @param globalRoot This is only present for if this is the global root
* @param pointer
* @param ownername
* @param readers
* @param writers
* @param entryWriterKey
*/
public FileTreeNode(Optional<TrieNode> globalRoot, RetrievedFilePointer pointer, String ownername,
Set<String> readers, Set<String> writers, Optional<SecretSigningKey> entryWriterKey) {
this.globalRoot = globalRoot;
this.pointer = pointer == null ? null : pointer.withWriter(entryWriterKey);
this.ownername = ownername;
this.readers = readers;
this.writers = writers;
this.entryWriterKey = entryWriterKey;
if (pointer == null)
props = new FileProperties("/", 0, LocalDateTime.MIN, false, Optional.empty());
else {
SymmetricKey parentKey = this.getParentKey();
props = pointer.fileAccess.getFileProperties(parentKey);
}
thumbnail = new NativeJSThumbnail();
}
public FileTreeNode(RetrievedFilePointer pointer, String ownername,
Set<String> readers, Set<String> writers, Optional<SecretSigningKey> entryWriterKey) {
this(Optional.empty(), pointer, ownername, readers, writers, entryWriterKey);
}
public FileTreeNode withTrieNode(TrieNode trie) {
return new FileTreeNode(Optional.of(trie), pointer, ownername, readers, writers, entryWriterKey);
}
@JsMethod
public boolean equals(Object other) {
if (other == null)
return false;
if (!(other instanceof FileTreeNode))
return false;
return pointer.equals(((FileTreeNode)other).getPointer());
}
public RetrievedFilePointer getPointer() {
return pointer;
}
public boolean isRoot() {
return props.name.equals("/");
}
public CompletableFuture<String> getPath(NetworkAccess network) {
return retrieveParent(network).thenCompose(parent -> {
if (!parent.isPresent() || parent.get().isRoot())
return CompletableFuture.completedFuture("/" + props.name);
return parent.get().getPath(network).thenApply(parentPath -> parentPath + "/" + props.name);
});
}
public CompletableFuture<Optional<FileTreeNode>> getDescendentByPath(String path, NetworkAccess network) {
if (path.length() == 0)
return CompletableFuture.completedFuture(Optional.of(this));
if (path.equals("/"))
if (isDirectory())
return CompletableFuture.completedFuture(Optional.of(this));
else
return CompletableFuture.completedFuture(Optional.empty());
if (path.startsWith("/"))
path = path.substring(1);
int slash = path.indexOf("/");
String prefix = slash > 0 ? path.substring(0, slash) : path;
String suffix = slash > 0 ? path.substring(slash + 1) : "";
return getChildren(network).thenCompose(children -> {
for (FileTreeNode child : children)
if (child.getFileProperties().name.equals(prefix)) {
return child.getDescendentByPath(suffix, network);
}
return CompletableFuture.completedFuture(Optional.empty());
});
}
/** Marks a file/directory and all its descendants as dirty. Directories are immediately cleaned,
* but files have all their keys except the actual data key cleaned. That is cleaned lazily, the next time it is modified
*
* @param network
* @param parent
* @param readersToRemove
* @return
* @throws IOException
*/
public CompletableFuture<FileTreeNode> makeDirty(NetworkAccess network, SafeRandom random,
FileTreeNode parent, Set<String> readersToRemove) {
if (!isWritable())
throw new IllegalStateException("You cannot mark a file as dirty without write access!");
if (isDirectory()) {
// create a new baseKey == subfoldersKey and make all descendants dirty
SymmetricKey newSubfoldersKey = SymmetricKey.random();
FilePointer ourNewPointer = pointer.filePointer.withBaseKey(newSubfoldersKey);
SymmetricKey newParentKey = SymmetricKey.random();
FileProperties props = getFileProperties();
// Create new DirAccess, but don't upload it
DirAccess newDirAccess = DirAccess.create(newSubfoldersKey, props, parent.pointer.filePointer.getLocation(),
parent.getParentKey(), newParentKey);
// re add children
DirAccess existing = (DirAccess) pointer.fileAccess;
List<FilePointer> subdirs = existing.getSubfolders().stream().map(link ->
new FilePointer(link.targetLocation(pointer.filePointer.baseKey),
Optional.empty(), link.target(pointer.filePointer.baseKey))).collect(Collectors.toList());
return newDirAccess.addSubdirsAndCommit(subdirs, newSubfoldersKey, ourNewPointer, getSigner(), network, random)
.thenCompose(updatedDirAccess -> {
SymmetricKey filesKey = existing.getFilesKey(pointer.filePointer.baseKey);
List<FilePointer> files = existing.getFiles().stream()
.map(link -> new FilePointer(link.targetLocation(filesKey), Optional.empty(), link.target(filesKey)))
.collect(Collectors.toList());
return updatedDirAccess.addFilesAndCommit(files, newSubfoldersKey, ourNewPointer, getSigner(), network, random)
.thenCompose(fullyUpdatedDirAccess -> {
readers.removeAll(readersToRemove);
RetrievedFilePointer ourNewRetrievedPointer = new RetrievedFilePointer(ourNewPointer, fullyUpdatedDirAccess);
FileTreeNode theNewUs = new FileTreeNode(ourNewRetrievedPointer,
ownername, readers, writers, entryWriterKey);
// clean all subtree keys except file dataKeys (lazily re-key and re-encrypt them)
return getChildren(network).thenCompose(children -> {
for (FileTreeNode child : children) {
child.makeDirty(network, random, theNewUs, readersToRemove);
}
// update pointer from parent to us
return ((DirAccess) parent.pointer.fileAccess)
.updateChildLink(parent.pointer.filePointer, this.pointer,
ourNewRetrievedPointer, getSigner(), network, random)
.thenApply(x -> theNewUs);
});
});
});
} else {
// create a new baseKey == parentKey and mark the metaDataKey as dirty
SymmetricKey parentKey = SymmetricKey.random();
return pointer.fileAccess.markDirty(pointer.filePointer, parentKey, network).thenCompose(newFileAccess -> {
// changing readers here will only affect the returned FileTreeNode, as the readers is derived from the entry point
TreeSet<String> newReaders = new TreeSet<>(readers);
newReaders.removeAll(readersToRemove);
RetrievedFilePointer newPointer = new RetrievedFilePointer(this.pointer.filePointer.withBaseKey(parentKey), newFileAccess);
// update link from parent folder to file to have new baseKey
return ((DirAccess) parent.pointer.fileAccess)
.updateChildLink(parent.pointer.filePointer, pointer, newPointer, getSigner(), network, random)
.thenApply(x -> new FileTreeNode(newPointer, ownername, newReaders, writers, entryWriterKey));
});
}
}
public CompletableFuture<Boolean> hasChildWithName(String name, NetworkAccess network) {
return getChildren(network)
.thenApply(children -> children.stream().filter(c -> c.props.name.equals(name)).findAny().isPresent());
}
public CompletableFuture<Boolean> removeChild(FileTreeNode child, NetworkAccess network) {
return ((DirAccess)pointer.fileAccess).removeChild(child.getPointer(), pointer.filePointer, getSigner(), network);
}
public CompletableFuture<FileTreeNode> addLinkTo(FileTreeNode file, NetworkAccess network, SafeRandom random) {
CompletableFuture<FileTreeNode> error = new CompletableFuture<>();
if (!this.isDirectory() || !this.isWritable()) {
error.completeExceptionally(new IllegalArgumentException("Can only add link toa writable directory!"));
return error;
}
String name = file.getFileProperties().name;
return hasChildWithName(name, network).thenCompose(hasChild -> {
if (hasChild) {
error.completeExceptionally(new IllegalStateException("Child already exists with name: " + name));
return error;
}
Location loc = file.getLocation();
DirAccess toUpdate = (DirAccess) pointer.fileAccess;
return (file.isDirectory() ?
toUpdate.addSubdirAndCommit(file.pointer.filePointer, this.getKey(),
pointer.filePointer, getSigner(), network, random) :
toUpdate.addFileAndCommit(file.pointer.filePointer, this.getKey(),
pointer.filePointer, getSigner(), network, random))
.thenApply(dirAccess -> new FileTreeNode(this.pointer, ownername, readers, writers, entryWriterKey));
});
}
@JsMethod
public String toLink() {
return pointer.filePointer.toLink();
}
@JsMethod
public boolean isWritable() {
return entryWriterKey.isPresent();
}
@JsMethod
public boolean isReadable() {
try {
pointer.fileAccess.getMetaKey(pointer.filePointer.baseKey);
return false;
} catch (Exception e) {}
return true;
}
public SymmetricKey getKey() {
return pointer.filePointer.baseKey;
}
public Location getLocation() {
return pointer.filePointer.getLocation();
}
private SigningKeyPair getSigner() {
if (! isWritable())
throw new IllegalStateException("Can only get a signer for a writable directory!");
return new SigningKeyPair(getLocation().writer, entryWriterKey.get());
}
public Set<Location> getChildrenLocations() {
if (!this.isDirectory())
return Collections.emptySet();
return ((DirAccess)pointer.fileAccess).getChildrenLocations(pointer.filePointer.baseKey);
}
public CompletableFuture<Optional<FileTreeNode>> retrieveParent(NetworkAccess network) {
if (pointer == null)
return CompletableFuture.completedFuture(Optional.empty());
SymmetricKey parentKey = getParentKey();
CompletableFuture<RetrievedFilePointer> parent = pointer.fileAccess.getParent(parentKey, network);
return parent.thenApply(parentRFP -> {
if (parentRFP == null)
return Optional.empty();
return Optional.of(new FileTreeNode(parentRFP, ownername, Collections.emptySet(), Collections.emptySet(), entryWriterKey));
});
}
public SymmetricKey getParentKey() {
SymmetricKey parentKey = pointer.filePointer.baseKey;
if (this.isDirectory())
try {
parentKey = pointer.fileAccess.getParentKey(parentKey);
} catch (Exception e) {
// if we don't have read access to this folder, then we must just have the parent key already
}
return parentKey;
}
@JsMethod
public CompletableFuture<Set<FileTreeNode>> getChildren(NetworkAccess network) {
if (globalRoot.isPresent())
return globalRoot.get().getChildren("/", network);
if (isReadable()) {
return retrieveChildren(network).thenApply(childrenRFPs -> {
Set<FileTreeNode> newChildren = childrenRFPs.stream()
.map(x -> new FileTreeNode(x, ownername, readers, writers, entryWriterKey))
.collect(Collectors.toSet());
return newChildren.stream().collect(Collectors.toSet());
});
}
throw new IllegalStateException("Unreadable FileTreeNode!");
}
private CompletableFuture<Set<RetrievedFilePointer>> retrieveChildren(NetworkAccess network) {
FilePointer filePointer = pointer.filePointer;
FileAccess fileAccess = pointer.fileAccess;
SymmetricKey rootDirKey = filePointer.baseKey;
if (isReadable())
return ((DirAccess) fileAccess).getChildren(network, rootDirKey);
throw new IllegalStateException("No credentials to retrieve children!");
}
public CompletableFuture<Boolean> cleanUnreachableChildren(NetworkAccess network) {
FilePointer filePointer = pointer.filePointer;
FileAccess fileAccess = pointer.fileAccess;
SymmetricKey rootDirKey = filePointer.baseKey;
if (isReadable())
return ((DirAccess) fileAccess).cleanUnreachableChildren(network, rootDirKey, filePointer, getSigner());
throw new IllegalStateException("No credentials to retrieve children!");
}
@JsMethod
public String getOwner() {
return ownername;
}
@JsMethod
public boolean isDirectory() {
boolean isNull = pointer == null;
return isNull || pointer.fileAccess.isDirectory();
}
public boolean isDirty() {
return pointer.fileAccess.isDirty(pointer.filePointer.baseKey);
}
public CompletableFuture<FileTreeNode> clean(NetworkAccess network, SafeRandom random,
FileTreeNode parent, peergos.shared.user.fs.Fragmenter fragmenter) {
if (!isDirty())
return CompletableFuture.completedFuture(this);
if (isDirectory()) {
throw new IllegalStateException("Unimplemented directory cleaning!");
} else {
FileProperties props = getFileProperties();
SymmetricKey baseKey = pointer.filePointer.baseKey;
// stream download and re-encrypt with new metaKey
return getInputStream(network, random, l -> {}).thenCompose(in -> {
byte[] tmp = new byte[16];
new Random().nextBytes(tmp);
String tmpFilename = ArrayOps.bytesToHex(tmp) + ".tmp";
CompletableFuture<Boolean> reuploaded = parent.uploadFileSection(tmpFilename, in, 0, props.size,
Optional.of(baseKey), network, random, l -> {}, fragmenter);
return reuploaded.thenCompose(upload -> parent.getDescendentByPath(tmpFilename, network))
.thenCompose(tmpChild -> tmpChild.get().rename(props.name, network, parent, true))
.thenCompose(rename -> parent.getDescendentByPath(props.name, network))
.thenApply(fileOpt -> fileOpt.get());
});
}
}
@JsMethod
public CompletableFuture<Boolean> uploadFileJS(String filename, AsyncReader fileData, int lengthHi, int lengthLow,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor, Fragmenter fragmenter) {
return uploadFile(filename, fileData, lengthLow + ((lengthHi & 0xFFFFFFFFL) << 32), network, random, monitor, fragmenter);
}
public CompletableFuture<Boolean> uploadFile(String filename, AsyncReader fileData, long length,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor, Fragmenter fragmenter) {
return uploadFileSection(filename, fileData, 0, length, Optional.empty(), network, random, monitor, fragmenter);
}
public CompletableFuture<Boolean> uploadFile(String filename,
AsyncReader fileData,
boolean isHidden,
long length,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor,
Fragmenter fragmenter) {
return uploadFileSection(filename, fileData, isHidden, 0, length, Optional.empty(), network, random, monitor, fragmenter);
}
public CompletableFuture<Boolean> uploadFileSection(String filename, AsyncReader fileData, long startIndex, long endIndex,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor, Fragmenter fragmenter) {
return uploadFileSection(filename, fileData, startIndex, endIndex, Optional.empty(), network, random, monitor, fragmenter);
}
public CompletableFuture<Boolean> uploadFileSection(String filename, AsyncReader fileData,
long startIndex, long endIndex,
Optional<SymmetricKey> baseKey,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor,
Fragmenter fragmenter) {
return uploadFileSection(filename, fileData, false, startIndex, endIndex, baseKey, network, random, monitor, fragmenter);
}
public CompletableFuture<Boolean> uploadFileSection(String filename, AsyncReader fileData,
boolean isHidden,
long startIndex, long endIndex,
Optional<SymmetricKey> baseKey,
NetworkAccess network,
SafeRandom random,
ProgressConsumer<Long> monitor,
Fragmenter fragmenter) {
if (!isLegalName(filename))
return CompletableFuture.completedFuture(false);
return getDescendentByPath(filename, network).thenCompose(childOpt -> {
if (childOpt.isPresent()) {
return updateExistingChild(childOpt.get(), fileData, startIndex, endIndex, network, random, monitor, fragmenter);
}
if (startIndex > 0) {
// TODO if startIndex > 0 prepend with a zero section
throw new IllegalStateException("Unimplemented!");
}
SymmetricKey fileKey = baseKey.orElseGet(SymmetricKey::random);
SymmetricKey fileMetaKey = SymmetricKey.random();
SymmetricKey rootRKey = pointer.filePointer.baseKey;
DirAccess dirAccess = (DirAccess) pointer.fileAccess;
SymmetricKey dirParentKey = dirAccess.getParentKey(rootRKey);
Location parentLocation = getLocation();
CompletableFuture<Boolean> result = new CompletableFuture<>();
int thumbnailSrcImageSize = startIndex == 0 && endIndex < Integer.MAX_VALUE ? (int)endIndex : 0;
generateThumbnail(network, fileData, thumbnailSrcImageSize, filename).thenAccept(thumbData -> {
fileData.reset().thenAccept(resetResult -> {
FileProperties fileProps = new FileProperties(filename, endIndex, LocalDateTime.now(), isHidden, Optional.of(thumbData));
FileUploader chunks = new FileUploader(filename, fileData, startIndex, endIndex, fileKey, fileMetaKey, parentLocation, dirParentKey, monitor, fileProps,
fragmenter);
byte[] mapKey = random.randomBytes(32);
Location nextChunkLocation = new Location(getLocation().owner, getLocation().writer, mapKey);
chunks.upload(network, random, parentLocation.owner, getSigner(), nextChunkLocation).thenAccept(fileLocation -> {
FilePointer filePointer = new FilePointer(fileLocation, Optional.empty(), fileKey);
dirAccess.addFileAndCommit(filePointer, rootRKey, pointer.filePointer, getSigner(), network, random)
.thenAccept(uploadResult -> result.complete(true));
});
});
});
return result;
});
}
private CompletableFuture<Boolean> updateExistingChild(FileTreeNode existingChild, AsyncReader fileData,
long inputStartIndex, long endIndex,
NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor, Fragmenter fragmenter) {
String filename = existingChild.getFileProperties().name;
System.out.println("Overwriting section [" + Long.toHexString(inputStartIndex) + ", " + Long.toHexString(endIndex) + "] of child with name: " + filename);
if (existingChild.isDirty()) {
CompletableFuture<FileTreeNode> clean = existingChild.clean(network, random, this, fragmenter);
return clean.thenCompose(x -> CompletableFuture.completedFuture(true));
}
Supplier<Location> locationSupplier = () -> new Location(getLocation().owner, getLocation().writer, random.randomBytes(32));
return CompletableFuture.completedFuture(existingChild)
.thenCompose(child -> {
FileProperties childProps = child.getFileProperties();
final AtomicLong filesSize = new AtomicLong(childProps.size);
FileRetriever retriever = child.getRetriever();
SymmetricKey baseKey = child.pointer.filePointer.baseKey;
SymmetricKey dataKey = child.pointer.fileAccess.getMetaKey(baseKey);
List<Long> startIndexes = new ArrayList<>();
for (long startIndex = inputStartIndex; startIndex < endIndex; startIndex = startIndex + Chunk.MAX_SIZE - (startIndex % Chunk.MAX_SIZE))
startIndexes.add(startIndex);
boolean identity = true;
BiFunction<Boolean, Long, CompletableFuture<Boolean>> composer = (id, startIndex) -> {
return retriever.getChunkInputStream(network, random, dataKey, startIndex, filesSize.get(), child.getLocation(), monitor)
.thenCompose(currentLocation -> {
CompletableFuture<Optional<Location>> locationAt = retriever
.getLocationAt(child.getLocation(), startIndex + Chunk.MAX_SIZE, dataKey, network);
return locationAt.thenCompose(location ->
CompletableFuture.completedFuture(new Pair<>(currentLocation, location)));
}
).thenCompose(pair -> {
if (!pair.left.isPresent()) {
CompletableFuture<Boolean> result = new CompletableFuture<>();
result.completeExceptionally(new IllegalStateException("Current chunk not present"));
return result;
}
LocatedChunk currentOriginal = pair.left.get();
Optional<Location> nextChunkLocationOpt = pair.right;
Location nextChunkLocation = nextChunkLocationOpt.orElseGet(locationSupplier);
System.out.println("********** Writing to chunk at mapkey: " + ArrayOps.bytesToHex(currentOriginal.location.getMapKey()) + " next: " + nextChunkLocation);
// modify chunk, re-encrypt and upload
int internalStart = (int) (startIndex % Chunk.MAX_SIZE);
int internalEnd = endIndex - (startIndex - internalStart) > Chunk.MAX_SIZE ?
Chunk.MAX_SIZE : (int) (endIndex - (startIndex - internalStart));
byte[] rawData = currentOriginal.chunk.data();
// extend data array if necessary
if (rawData.length < internalEnd)
rawData = Arrays.copyOfRange(rawData, 0, internalEnd);
byte[] raw = rawData;
return fileData.readIntoArray(raw, internalStart, internalEnd - internalStart).thenCompose(read -> {
byte[] nonce = random.randomBytes(TweetNaCl.SECRETBOX_NONCE_BYTES);
Chunk updated = new Chunk(raw, dataKey, currentOriginal.location.getMapKey(), nonce);
LocatedChunk located = new LocatedChunk(currentOriginal.location, updated);
long currentSize = filesSize.get();
FileProperties newProps = new FileProperties(childProps.name, endIndex > currentSize ? endIndex : currentSize,
LocalDateTime.now(), childProps.isHidden, childProps.thumbnail);
CompletableFuture<Boolean> chunkUploaded = FileUploader.uploadChunk(getSigner(),
newProps, getLocation(), getParentKey(), baseKey, located,
fragmenter,
nextChunkLocation, network, monitor);
return chunkUploaded.thenCompose(isUploaded -> {
if (!isUploaded)
return CompletableFuture.completedFuture(false);
//update indices to be relative to next chunk
long updatedLength = startIndex + internalEnd - internalStart;
if (updatedLength > filesSize.get()) {
filesSize.set(updatedLength);
if (updatedLength > Chunk.MAX_SIZE) {
// update file size in FileProperties of first chunk
CompletableFuture<Boolean> updatedSize = getChildren(network).thenCompose(children -> {
Optional<FileTreeNode> updatedChild = children.stream()
.filter(f -> f.getFileProperties().name.equals(filename))
.findAny();
return updatedChild.get().setProperties(child.getFileProperties().withSize(endIndex), network, this);
});
}
}
return CompletableFuture.completedFuture(true);
});
});
});
};
BiFunction<Boolean, Boolean, Boolean> combiner = (left, right) -> left && right;
return Futures.reduceAll(startIndexes, identity, composer, combiner);
});
}
static boolean isLegalName(String name) {
return !name.contains("/");
}
@JsMethod
public CompletableFuture<FilePointer> mkdir(String newFolderName, NetworkAccess network, boolean isSystemFolder,
SafeRandom random) throws IOException {
return mkdir(newFolderName, network, null, isSystemFolder, random);
}
public CompletableFuture<FilePointer> mkdir(String newFolderName,
NetworkAccess network,
SymmetricKey requestedBaseSymmetricKey,
boolean isSystemFolder,
SafeRandom random) {
CompletableFuture<FilePointer> result = new CompletableFuture<>();
if (!this.isDirectory()) {
result.completeExceptionally(new IllegalStateException("Cannot mkdir in a file!"));
return result;
}
if (!isLegalName(newFolderName)) {
result.completeExceptionally(new IllegalStateException("Illegal directory name: " + newFolderName));
return result;
}
return hasChildWithName(newFolderName, network).thenCompose(hasChild -> {
if (hasChild) {
result.completeExceptionally(new IllegalStateException("Child already exists with name: " + newFolderName));
return result;
}
FilePointer dirPointer = pointer.filePointer;
DirAccess dirAccess = (DirAccess) pointer.fileAccess;
SymmetricKey rootDirKey = dirPointer.baseKey;
return dirAccess.mkdir(newFolderName, network, dirPointer.location.owner, getSigner(), dirPointer.getLocation().getMapKey(), rootDirKey,
requestedBaseSymmetricKey, isSystemFolder, random);
});
}
@JsMethod
public CompletableFuture<Boolean> rename(String newFilename, NetworkAccess network, FileTreeNode parent) {
return rename(newFilename, network, parent, false);
}
public CompletableFuture<Boolean> rename(String newFilename, NetworkAccess network,
FileTreeNode parent, boolean overwrite) {
if (! isLegalName(newFilename))
return CompletableFuture.completedFuture(false);
CompletableFuture<Optional<FileTreeNode>> childExists = parent == null ?
CompletableFuture.completedFuture(Optional.empty()) :
parent.getDescendentByPath(newFilename, network);
return childExists
.thenCompose(existing -> {
if (existing.isPresent() && !overwrite)
return CompletableFuture.completedFuture(false);
return ((overwrite && existing.isPresent()) ?
existing.get().remove(network, parent) :
CompletableFuture.completedFuture(true)).thenCompose(res -> {
//get current props
FilePointer filePointer = pointer.filePointer;
SymmetricKey baseKey = filePointer.baseKey;
FileAccess fileAccess = pointer.fileAccess;
SymmetricKey key = this.isDirectory() ? fileAccess.getParentKey(baseKey) : baseKey;
FileProperties currentProps = fileAccess.getFileProperties(key);
FileProperties newProps = new FileProperties(newFilename, currentProps.size, currentProps.modified, currentProps.isHidden, currentProps.thumbnail);
return fileAccess.rename(writableFilePointer(), newProps, network);
});
});
}
public CompletableFuture<Boolean> setProperties(FileProperties updatedProperties, NetworkAccess network, FileTreeNode parent) {
String newName = updatedProperties.name;
CompletableFuture<Boolean> result = new CompletableFuture<>();
if (!isLegalName(newName)) {
result.completeExceptionally(new IllegalArgumentException("Illegal file name: " + newName));
return result;
}
return (parent == null ? CompletableFuture.completedFuture(false) : parent.hasChildWithName(newName, network)).thenCompose(hasChild -> {
if (hasChild && parent!= null && !parent.getChildrenLocations().stream()
.map(l -> new ByteArrayWrapper(l.getMapKey()))
.collect(Collectors.toSet())
.contains(new ByteArrayWrapper(pointer.filePointer.getLocation().getMapKey()))) {
result.completeExceptionally(new IllegalStateException("Cannot rename to same name as an existing file"));
return result;
}
FileAccess fileAccess = pointer.fileAccess;
return fileAccess.rename(writableFilePointer(), updatedProperties, network);
});
}
private FilePointer writableFilePointer() {
FilePointer filePointer = pointer.filePointer;
SymmetricKey baseKey = filePointer.baseKey;
return new FilePointer(filePointer.location, entryWriterKey, baseKey);
}
public Optional<SecretSigningKey> getEntryWriterKey() {
return entryWriterKey;
}
@JsMethod
public CompletableFuture<FileTreeNode> copyTo(FileTreeNode target, NetworkAccess network, SafeRandom random) {
CompletableFuture<FileTreeNode> result = new CompletableFuture<>();
if (! target.isDirectory()) {
result.completeExceptionally(new IllegalStateException("CopyTo target " + target + " must be a directory"));
return result;
}
return target.hasChildWithName(getFileProperties().name, network).thenCompose(childExists -> {
if (childExists) {
result.completeExceptionally(new IllegalStateException("CopyTo target " + target + " already has child with name " + getFileProperties().name));
return result;
}
//make new FileTreeNode pointing to the same file, but with a different location
byte[] newMapKey = new byte[32];
random.randombytes(newMapKey, 0, 32);
SymmetricKey ourBaseKey = this.getKey();
// a file baseKey is the key for the chunk, which hasn't changed, so this must stay the same
SymmetricKey newBaseKey = this.isDirectory() ? SymmetricKey.random() : ourBaseKey;
FilePointer newRFP = new FilePointer(target.getLocation().owner, target.getLocation().writer, newMapKey, newBaseKey);
Location newParentLocation = target.getLocation();
SymmetricKey newParentParentKey = target.getParentKey();
return pointer.fileAccess.copyTo(ourBaseKey, newBaseKey, newParentLocation, newParentParentKey,
getSigner(), newMapKey, network)
.thenCompose(newAccess -> {
// upload new metadatablob
RetrievedFilePointer newRetrievedFilePointer = new RetrievedFilePointer(newRFP, newAccess);
FileTreeNode newFileTreeNode = new FileTreeNode(newRetrievedFilePointer, target.getOwner(),
Collections.emptySet(), Collections.emptySet(), target.getEntryWriterKey());
return target.addLinkTo(newFileTreeNode, network, random);
});
});
}
@JsMethod
public CompletableFuture<Boolean> remove(NetworkAccess network, FileTreeNode parent) {
Supplier<CompletableFuture<Boolean>> supplier = () -> new RetrievedFilePointer(writableFilePointer(), pointer.fileAccess)
.remove(network, null, getSigner());
if (parent != null) {
return parent.removeChild(this, network)
.thenCompose(x -> supplier.get());
}
return supplier.get();
}
public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network, SafeRandom random,
ProgressConsumer<Long> monitor) {
return getInputStream(network, random, getFileProperties().size, monitor);
}
@JsMethod
public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network, SafeRandom random,
int fileSizeHi, int fileSizeLow,
ProgressConsumer<Long> monitor) {
return getInputStream(network, random, fileSizeLow + ((fileSizeHi & 0xFFFFFFFFL) << 32), monitor);
}
public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network, SafeRandom random,
long fileSize, ProgressConsumer<Long> monitor) {
SymmetricKey baseKey = pointer.filePointer.baseKey;
SymmetricKey dataKey = pointer.fileAccess.getMetaKey(baseKey);
return pointer.fileAccess.retriever().getFile(network, random, dataKey, fileSize, getLocation(), monitor);
}
private FileRetriever getRetriever() {
return pointer.fileAccess.retriever();
}
@JsMethod
public String getBase64Thumbnail() {
Optional<byte[]> thumbnail = props.thumbnail;
if(thumbnail.isPresent()){
String base64Data = Base64.getEncoder().encodeToString(thumbnail.get());
return "data:image/png;base64," + base64Data;
}else{
return "";
}
}
@JsMethod
public FileProperties getFileProperties() {
return props;
}
public String getName() {
return getFileProperties().name;
}
public long getSize() {
return getFileProperties().size;
}
public String toString() {
return getFileProperties().name;
}
public static FileTreeNode createRoot(TrieNode root) {
return new FileTreeNode(Optional.of(root), null, null, Collections.EMPTY_SET, Collections.EMPTY_SET, null);
}
private CompletableFuture<byte[]> generateThumbnail(NetworkAccess network, AsyncReader fileData, int fileSize, String filename)
{
CompletableFuture<byte[]> fut = new CompletableFuture<>();
if(network.isJavascript() && fileSize > 0) {
isImage(fileData).thenAccept(isThumbnail -> {
if(isThumbnail) {
thumbnail.generateThumbnail(fileData, fileSize, filename).thenAccept(base64Str -> {
byte[] bytesOfData = Base64.getDecoder().decode(base64Str);
fut.complete(bytesOfData);
});
} else{
fut.complete(new byte[0]);
}
});
} else {
fut.complete(new byte[0]);
}
return fut;
}
private CompletableFuture<Boolean> isImage(AsyncReader imageBlob)
{
CompletableFuture<Boolean> result = new CompletableFuture<>();
byte[] data = new byte[HEADER_BYTES_TO_IDENTIFY_IMAGE_FILE];
imageBlob.readIntoArray(data, 0, HEADER_BYTES_TO_IDENTIFY_IMAGE_FILE).thenAccept(numBytesRead -> {
imageBlob.reset().thenAccept(resetResult -> {
if(numBytesRead < HEADER_BYTES_TO_IDENTIFY_IMAGE_FILE) {
result.complete(false);
}else {
byte[] tempBytes = Arrays.copyOfRange(data, 0, 2);
if (!compareArrayContents(Arrays.copyOfRange(data, 0, BMP.length), BMP)
&& !compareArrayContents(Arrays.copyOfRange(data, 0, GIF.length), GIF)
&& !compareArrayContents(Arrays.copyOfRange(data, 0, PNG.length), PNG)
&& !compareArrayContents(Arrays.copyOfRange(data, 0, 2), JPEG)) {
result.complete(false);
}else {
result.complete(true);
}
}
});
});
return result;
}
private boolean compareArrayContents(byte[] a, int[] a2) {
if (a==null || a2==null){
return false;
}
int length = a.length;
if (a2.length != length){
return false;
}
for (int i=0; i<length; i++) {
if (a[i] != a2[i]) {
return false;
}
}
return true;
}
private static InputStream NULL_STREAM = new InputStream() {
@Override
public int read() throws IOException {
return 0;
}
};
}