<NME> FileWrapper.java
<BEF> package peergos.shared.user.fs;
import java.nio.file.*;
import java.util.logging.*;

import jsinterop.annotations.*;
import peergos.shared.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.hash.*;
import peergos.shared.crypto.random.*;
import peergos.shared.crypto.symmetric.*;
import peergos.shared.io.ipfs.multihash.*;
import peergos.shared.storage.*;
import peergos.shared.storage.auth.*;
import peergos.shared.user.*;
import peergos.shared.user.fs.cryptree.*;
import peergos.shared.user.fs.transaction.*;
import peergos.shared.util.*;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.Graphics2D;
import java.awt.AlphaComposite;
import java.awt.RenderingHints;
import java.io.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.stream.*;

/** This class is used to read and modify files and directories and represents a single file or directory and the keys
 *  to access it.
 *
 */
public class FileWrapper {
    public static final long THUMBNAIL_PROGRESS_OFFSET = 20*1024;
	private static final Logger LOG = Logger.getGlobal();

    private final static int THUMBNAIL_SIZE = 400;
    private static final NativeJSThumbnail thumbnail = new NativeJSThumbnail();

    private final RetrievedCapability pointer;
    private final Optional<RetrievedCapability> linkPointer;
    private final Optional<SigningPrivateKeyAndPublicHash> entryWriter;
    private final FileProperties props;
    private final String ownername;
    private final Optional<TrieNode> capTrie;
    public final Snapshot version;
    private final boolean isWritable;
    private AtomicBoolean modified = new AtomicBoolean(); // This only used as a guard against concurrent modifications

    /**
     *
     * @param capTrie This is only present if this is the global root, or this is read only
     * @param pointer
     * @param ownername
     */
    public FileWrapper(Optional<TrieNode> capTrie,
                       RetrievedCapability pointer,
                       Optional<RetrievedCapability> linkPointer,
                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,
                       String ownername,
                       Snapshot version) {
        this.capTrie = capTrie;
        this.pointer = pointer;
        this.linkPointer = linkPointer;
        this.entryWriter = entryWriter;
        this.ownername = ownername;
        this.version = version;
        this.isWritable = pointer != null &&
                pointer.capability instanceof WritableAbsoluteCapability ||
                entryWriter.map(s -> s.publicKeyHash.equals(pointer.capability.writer)).orElse(false);
        if (pointer == null)
            props = new FileProperties("/", true, false, "", 0, LocalDateTime.MIN, LocalDateTime.MIN, false, Optional.empty(), Optional.empty());
        else {
            SymmetricKey parentKey = this.getParentKey();
            FileProperties directProps = pointer.fileAccess.getProperties(parentKey);
            if (linkPointer.isPresent()) {
                RetrievedCapability link = linkPointer.get();
                FileProperties linkProps = link.getProperties();
                this.props = directProps.withLink(linkProps);
            } else {
                this.props = directProps;
            }
        }
        if (pointer != null && ! version.contains(pointer.capability.writer))
            throw new IllegalStateException("File version doesn't include its own writer!");
        if (isWritable() && !signingPair().publicKeyHash.equals(pointer.capability.writer))
            throw new IllegalStateException("Invalid FileWrapper! public writing keys don't match!");
    }

    public FileWrapper(RetrievedCapability pointer,
                       Optional<RetrievedCapability> linkPointer,
                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,
                       String ownername,
                       Snapshot version) {
        this(Optional.empty(), pointer, linkPointer, entryWriter, ownername, version);
    }

    public FileWrapper withTrieNode(TrieNode trie) {
        return new FileWrapper(Optional.of(trie), pointer, linkPointer, entryWriter, ownername, version);
    }

    public FileWrapper withTrieNodeOpt(Optional<TrieNode> trie) {
        return new FileWrapper(trie, pointer, linkPointer, entryWriter, ownername, version);
    }

    public FileWrapper withLinkPointer(Optional<RetrievedCapability> link) {
        return new FileWrapper(capTrie, pointer, link, entryWriter, ownername, version);
    }

    public FileWrapper withVersion(Snapshot version) {
        return new FileWrapper(capTrie, pointer, linkPointer, entryWriter, ownername, version);
    }

    public CommittedWriterData getVersionRoot() {
        return version.get(writer());
    }

    @JsMethod
    public CompletableFuture<FileWrapper> getLatest(NetworkAccess network) {
        return network.synchronizer.getValue(owner(), writer())
                .thenCompose(latest -> getUpdated(latest, network));

    }

    @JsMethod
    public boolean samePointer(FileWrapper other) {
        return pointer.equals(other.getPointer());
    }

    public CompletableFuture<FileWrapper> getUpdated(NetworkAccess network) {
        return network.synchronizer.getValue(owner(), writer())
                .thenCompose(v -> getUpdated(v, network));
    }

    public CompletableFuture<FileWrapper> getUpdated(Snapshot version, NetworkAccess network) {
        return version.withWriter(owner(), writer(), network).thenCompose(v -> {
            if (this.version.get(writer()).equals(v.get(writer())))
                return CompletableFuture.completedFuture(this);
            return network.getFile(v, pointer.capability, entryWriter, ownername)
                    .thenApply(Optional::get)
                    .thenApply(f -> f.withTrieNodeOpt(capTrie));
        });
    }

    public PublicKeyHash owner() {
        try {
            return pointer.capability.owner;
        } catch (Exception e)  {
            System.out.println();
            throw e;
        }
    }

    public PublicKeyHash writer() {
        return pointer.capability.writer;
    }

    public RetrievedCapability getPointer() {
        return pointer;
    }

    public RetrievedCapability getLinkPointer() {
        return linkPointer.get();
    }

    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);
        });
    }

    @JsMethod
    public CompletableFuture<Multihash> getContentHash(NetworkAccess network, Crypto crypto) {
        return getInputStream(network, crypto, x -> {})
                .thenCompose(reader -> crypto.hasher.hash(reader, getSize()));
    }

    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,
                                                                        Hasher hasher,
                                                                        NetworkAccess network) {
        return getDescendentByPath(path, version, hasher, network);
    }

    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,
                                                                        Snapshot version,
                                                                        Hasher hasher,
                                                                        NetworkAccess network) {
        ensureUnmodified();
        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());

        Path canon = PathUtil.get(path);
        return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {
            if (child.isPresent())
                return child.get().getDescendentByPath(canon.subpath(1, canon.getNameCount()).toString(), child.get().version, hasher, network);
            return CompletableFuture.completedFuture(Optional.empty());
        });
    }

    private void ensureUnmodified() {
        if (modified.get())
            throw new IllegalStateException("This file has already been modified, use the returned instance");
    }

    private void setModified() {
        if (modified.get())
            throw new IllegalStateException("This file has already been modified, use the returned instance");
        modified.set(true);
    }

    public CompletableFuture<Snapshot> updateChildLinks(
            Snapshot version,
            Committer committer,
            Collection<Pair<AbsoluteCapability, NamedAbsoluteCapability>> childCases,
            NetworkAccess network,
            SafeRandom random,
            Hasher hasher) {
        return pointer.fileAccess
                .updateChildLinks(version, committer, (WritableAbsoluteCapability) pointer.capability, signingPair(),
                        childCases, network, random, hasher);
    }

    public CompletableFuture<Boolean> hasChildWithName(Snapshot version, String name, Hasher hasher, NetworkAccess network) {
        ensureUnmodified();
        return getChild(version, name, network)
                .thenApply(Optional::isPresent);
    }

    /**
     *
     * @param child
     * @param network
     * @return Updated version of this directory without the child
     */
    public CompletableFuture<FileWrapper> removeChild(FileWrapper child, NetworkAccess network, SafeRandom random, Hasher hasher) {
        setModified();
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (cwd, committer) -> pointer.fileAccess
                .removeChildren(cwd, committer, Arrays.asList(child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher))
                .thenCompose(newRoot -> getUpdated(newRoot, network));
    }

    public CompletableFuture<Snapshot> removeChild(Snapshot version,
                                                   Committer committer,
                                                   FileWrapper child,
                                                   NetworkAccess network,
                                                   SafeRandom random,
                                                   Hasher hasher) {
        return pointer.fileAccess.removeChildren(version, committer,
                Arrays.asList(child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher);
    }

    @JsMethod
    public String toLink() {
        return pointer.capability.readOnly().toLink();
    }

    @JsMethod
    public String toWritableLink() {
        if (! isWritable())
            throw new IllegalStateException("You do not have write access to " + getName());
        return pointer.capability.toLink();
    }

    @JsMethod
    public boolean isWritable() {
        return isWritable;
    }

    @JsMethod
    public boolean isReadable() {
        return pointer.fileAccess.isReadable(pointer.capability.rBaseKey);
    }

    public SymmetricKey getKey() {
        return pointer.capability.rBaseKey;
    }

    public Location getLocation() {
        return new Location(pointer.capability.owner, pointer.capability.writer, pointer.capability.getMapKey());
    }

    public CompletableFuture<Set<NamedAbsoluteCapability>> getChildrenCapabilities(Hasher hasher, NetworkAccess network) {
        ensureUnmodified();
        if (!this.isDirectory())
            return CompletableFuture.completedFuture(Collections.emptySet());
        return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network);
    }

    public CompletableFuture<Optional<FileWrapper>> retrieveParent(NetworkAccess network) {
        ensureUnmodified();
        return retrieveParent(linkPointer.orElse(pointer), ownername, version, network);
    }

    public CompletableFuture<Optional<RetrievedCapability>> getAnyLinkPointer(NetworkAccess network) {
        if (pointer == null)
            return CompletableFuture.completedFuture(Optional.empty());
        AbsoluteCapability cap = pointer.capability;
        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);
        return parent.thenApply(parentRFP -> {
            if (parentRFP == null)
                return Optional.empty();
            FileProperties parentProps = parentRFP.getProperties();
            if (! parentProps.isLink)
                return Optional.empty();
            return Optional.of(parentRFP);
        });
    }

    public static CompletableFuture<Optional<FileWrapper>> retrieveParent(RetrievedCapability pointer,
                                                                          String ownerName,
                                                                          Snapshot version,
                                                                          NetworkAccess network) {
        if (pointer == null)
            return CompletableFuture.completedFuture(Optional.empty());
        AbsoluteCapability cap = pointer.capability;
        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);
        return parent.thenCompose(parentRFP -> {
            if (parentRFP == null)
                return Futures.of(Optional.empty());
            FileProperties parentProps = parentRFP.getProperties();
            if (! parentProps.isLink)
                return version.withWriter(parentRFP.capability.owner, parentRFP.capability.writer, network)
                        .thenApply(fullVersion -> Optional.of(new FileWrapper(parentRFP, Optional.empty(),
                                Optional.empty(), ownerName, fullVersion)));
            return retrieveParent(parentRFP, ownerName, version, network);
        });
    }

    @JsMethod
    public boolean isUserRoot() {
        if (pointer == null)
            return false;
        return ! pointer.fileAccess.hasParentLink(pointer.capability.rBaseKey);
    }

    public SymmetricKey getParentKey() {
        return pointer.getParentKey();
    }

    private Optional<SigningPrivateKeyAndPublicHash> getChildsEntryWriter() {
        return pointer.capability.wBaseKey
                .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));
    }

    @JsMethod
    public CompletableFuture<Set<FileWrapper>> getChildren(Hasher hasher, NetworkAccess network) {
        if (capTrie.isPresent())
            return capTrie.get().getChildren("", hasher, network);
        return getChildren(version, hasher, network);
    }

    public CompletableFuture<Set<FileWrapper>> getChildren(Snapshot version, Hasher hasher, NetworkAccess network) {
        if (capTrie.isPresent())
            return capTrie.get().getChildren("", hasher, version.merge(this.version), network);
        if (isReadable()) {
            Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey
                    .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));
            return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network)
                    .thenCompose(childCaps -> getFiles(owner(), childCaps, childsEntryWriter, ownername, network, version));
        }
        throw new IllegalStateException("Unreadable FileWrapper!");
    }

    private static CompletableFuture<Set<FileWrapper>> getFiles(PublicKeyHash owner,
                                                                Set<NamedAbsoluteCapability> caps,
                                                                Optional<SigningPrivateKeyAndPublicHash> entryWriter,
                                                                String ownername,
                                                                NetworkAccess network,
                                                                Snapshot version) {
        Set<PublicKeyHash> childWriters = caps.stream()
                .map(c -> c.cap.writer)
                .collect(Collectors.toSet());
        return version.withWriters(owner, childWriters, network)
                .thenCompose(fullVersion -> network.retrieveAllMetadata(caps.stream().map(n -> n.cap).collect(Collectors.toList()), fullVersion)
                        .thenCompose(rcs -> Futures.combineAll(rcs.stream()
                                .map(rc -> {
                                    FileProperties props = rc.getProperties();
                                    if (! props.isLink)
                                        return Futures.of(new FileWrapper(rc, Optional.empty(), entryWriter, ownername, fullVersion));
                                    return NetworkAccess.getFileFromLink(owner, rc, entryWriter, ownername, network, version);
                                })
                                .collect(Collectors.toSet()))));
    }

    @JsMethod
    public CompletableFuture<Optional<FileWrapper>> getChild(String name, Hasher hasher, NetworkAccess network) {
        return getChild(version, name, network);
    }

    public CompletableFuture<Optional<FileWrapper>> getChild(Snapshot version,
                                                             String name,
                                                             NetworkAccess network) {
        if (capTrie.isPresent())
            return capTrie.get().getByPath("/" + name, version.merge(this.version), network.hasher, network);
        return pointer.fileAccess.getChild(name, pointer.capability, version, network.hasher, network)
                .thenCompose(rcOpt -> {
                    if (rcOpt.isEmpty())
                        return Futures.of(Optional.empty());
                    RetrievedCapability rc = rcOpt.get();
                    FileProperties props = rc.getProperties();
                    Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey
                            .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));
                    if (! props.isLink)
                        return version.withWriter(owner(), rc.capability.writer, network)
                                .thenApply(fullVersion -> Optional.of(new FileWrapper(rc, Optional.empty(),
                                        childsEntryWriter, ownername, fullVersion)));
                    return version.withWriter(owner(), rc.capability.writer, network)
                            .thenCompose(fullVersion ->
                                    NetworkAccess.getFileFromLink(owner(), rc, childsEntryWriter, ownername, network, fullVersion)
                                            .thenApply(Optional::of));
                });
    }

    @JsMethod
    public String getOwnerName() {
        return ownername;
    }

    @JsMethod
    public boolean isDirectory() {
        boolean isNull = pointer == null;
        return isNull || pointer.fileAccess.isDirectory();
    }

    public boolean isLink() {
        return linkPointer.isPresent();
    }

    public boolean isDirty() {
        ensureUnmodified();
        return pointer.fileAccess.isDirty(pointer.capability.rBaseKey);
    }

    /**
     *
     * @param current
     * @param committer
     * @param network
     * @param crypto
     * @return updated cleaned version
     */
    public CompletableFuture<Pair<FileWrapper, Snapshot>> clean(Snapshot current,
                                                                Committer committer,
                                                                NetworkAccess network,
                                                                Crypto crypto) {
        if (!isDirty())
            return CompletableFuture.completedFuture(new Pair<>(this, current));
        if (isDirectory()) {
            throw new IllegalStateException("Directories are never dirty (they are cleaned immediately)!");
        } else {
            WritableAbsoluteCapability currentCap = writableFilePointer();
            boolean isLink = isLink();
            Optional<RelativeCapability> parentCap = pointer.fileAccess.getParentCapability(pointer.capability.rBaseKey);
            Location parentOrLinkLocation = isLink ?
                    linkPointer.get().capability.getLocation() :
                    parentCap.get().getLocation(owner(), writer());
            SymmetricKey parentOrLinkParentKey = isLink ?
                    linkPointer.get().getParentKey() :
                    parentCap.get().rBaseKey;
            Optional<Bat> parentOrLinkBat = isLink ?
                    linkPointer.get().capability.bat :
                    parentCap.get().bat;
            return pointer.fileAccess.cleanAndCommit(current, committer, currentCap, currentCap, props.streamSecret,
                    Optional.empty(), signingPair(), SymmetricKey.random(),
                    parentOrLinkLocation, parentOrLinkBat, parentOrLinkParentKey, network, crypto)
                    .thenCompose(cwd -> {
                        setModified();
                        return getUpdated(cwd, network).thenApply(updated -> new Pair<>(updated, cwd));
                    });
        }
    }

    public CompletableFuture<Pair<byte[], Optional<Bat>>> getMapKey(long offset, NetworkAccess network, Crypto crypto) {
        CryptreeNode fileAccess = pointer.fileAccess;
        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)
                .thenCompose(retriever -> retriever
                        .getMapLabelAt(version.get(writer()).props, writableFilePointer(),
                                getFileProperties().streamSecret, offset, crypto.hasher, network)
                        .thenApply(Optional::get));
    }

    @JsMethod
    public CompletableFuture<FileWrapper> truncate(long newSize, NetworkAccess network, Crypto crypto) {
        if (getSize() <= newSize)
            return Futures.of(this);
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->
                truncate(current, committer, newSize, network, crypto)
        ).thenCompose(finished -> getUpdated(finished, network));
    }

    public CompletableFuture<Snapshot> truncate(Snapshot initialVersion, Committer committer, long newSize, NetworkAccess network, Crypto crypto) {
        if (isDirectory())
            return Futures.errored(new IllegalStateException("You cannot truncate a directory!"));
        FileProperties props = getFileProperties();
        if (props.size <= newSize)
            return CompletableFuture.completedFuture(initialVersion);

        return initialVersion.withWriter(owner(), writer(), network)
                .thenCompose(snapshot -> getMapKey(newSize, network, crypto).thenCompose(endMapKey ->
                        getInputStream(snapshot.get(writer()).props, network, crypto, props.size, x -> {}).thenCompose(originalReader -> {
                            long startOfLastChunk = newSize - (newSize % Chunk.MAX_SIZE);
                            return originalReader.seek(startOfLastChunk).thenCompose(seekedOriginal -> {
                                byte[] lastChunk = new byte[(int)(newSize % Chunk.MAX_SIZE)];
                                return seekedOriginal.readIntoArray(lastChunk, 0, lastChunk.length).thenCompose(read -> {
                                    if (newSize <= Chunk.MAX_SIZE)
                                        return CompletableFuture.completedFuture(snapshot);
                                    int currentChunk = (int) (newSize / Chunk.MAX_SIZE);
                                    return IpfsTransaction.call(owner(), tid ->
                                                    deleteFileChunks(props.streamSecret.get(), props.chunkCount() - currentChunk, writableFilePointer().withMapKey(endMapKey.left, endMapKey.right),
                                                            signingPair(), tid, crypto.hasher, network, snapshot, committer),
                                            network.dhtClient);
                                }).thenCompose(deleted -> pointer.fileAccess.updateProperties(deleted, committer, writableFilePointer(),
                                        entryWriter, props.withSize(startOfLastChunk), network).thenCompose(resized ->
                                        getUpdated(resized, network).thenCompose(f -> f.clean(resized, committer, network, crypto)
                                                .thenCompose(p -> p.left.overwriteSection(p.right, committer,
                                                AsyncReader.build(lastChunk), startOfLastChunk, newSize, network, crypto, x -> {})))));
                            });
                        }))
                );
    }

    public static int getNumberOfChunks(long size) {
        if (size == 0)
            return 1;
        return (int)((size + Chunk.MAX_SIZE - 1)/Chunk.MAX_SIZE);
    }

    public List<Location> generateChildLocationsFromSize(long fileSize, SafeRandom random) {
        return generateChildLocations(getNumberOfChunks(fileSize), random);
    }

    public List<Location> generateChildLocations(int numberOfChunks,
                                                        SafeRandom random) {
        return IntStream.range(0, numberOfChunks + 1) //have to have one extra location
                .mapToObj(e -> new Location(owner(), writer(), random.randomBytes(32)))
                .collect(Collectors.toList());
    }

    @JsMethod
    public CompletableFuture<FileWrapper> appendFileJS(String filename,
                                                       AsyncReader fileData,
                                                       int lengthHi,
                                                       int lengthLow,
                                                       NetworkAccess network,
                                                       Crypto crypto,
                                                       ProgressConsumer<Long> monitor) {
        long fileSize = LongUtil.intsToLong(lengthHi, lengthLow);
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (s, committer) -> getChild(s, filename, network).thenCompose(childOpt -> {
                if (childOpt.isEmpty()) {
                    throw new IllegalStateException("File does not exists with name " + filename);
                } else {
                    FileProperties props = childOpt.get().getFileProperties();
                    if (props.isHidden) {
                        throw new IllegalStateException("File is hidden " + filename);
                    }
                    long startIndex = props.size;
                    return uploadFileSection(s, committer, filename, fileData,
                            false, startIndex, startIndex + fileSize, Optional.empty(), false, true, false,
                            network, crypto, monitor, crypto.random.randomBytes(32), Optional.empty(),
                            Optional.of(Bat.random(crypto.random)), mirrorBatId());
                }
        })).thenCompose(finished -> getUpdated(finished, network));
    }
        @JsMethod
    public CompletableFuture<FileWrapper> uploadFileJS(String filename,
                                                       AsyncReader fileData,
                                                       int lengthHi,
                                                       int lengthLow,
                                                       boolean overwriteExisting,
                                                       Optional<BatId> mirrorBat,
                                                       NetworkAccess network,
                                                       Crypto crypto,
                                                       ProgressConsumer<Long> monitor,
                                                       TransactionService transactions,
                                                       Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile) {
        FileUploadProperties fileProps = new FileUploadProperties(filename, fileData, lengthHi, lengthLow, false, overwriteExisting, monitor);
        FolderUploadProperties currentFolder = new FolderUploadProperties(Collections.emptyList(), Collections.singletonList(fileProps));
        return uploadSubtree(Stream.of(currentFolder), mirrorBat, network, crypto, transactions, resumeFile, () -> true);
    }

    private CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> resumeUpload(FileUploadTransaction txn,
                                                                                              AsyncReader data,
                                                                                              ProgressConsumer<Long> monitor,
                                                                                              Snapshot s,
                                                                                              Committer c,
                                                                                              NetworkAccess network,
                                                                                              Crypto crypto) {
        RelativeCapability fromParent = writableFilePointer().relativise(txn.writeCap());
        FileProperties props = txn.props;
        // first find how many chunks were already uploaded, then seek reader to that offset and continue
        return findFirstAbsentChunkIndex(0, txn.streamSecret(), txn.getFirstLocation(), s, network, crypto)
                .thenCompose(startChunkIndex -> {
                    monitor.accept(startChunkIndex * Chunk.MAX_SIZE);
                    FileUploader uploader = new FileUploader(txn.targetFilename(), data, startChunkIndex*Chunk.MAX_SIZE,
                            txn.size(), txn.baseKey, txn.dataKey, getLocation(), getPointer().capability.bat, getParentKey(),
                            monitor, props, txn.getFirstLocation().getMapKey(), txn.firstBat);
                    return uploader.uploadFrom(s, c, network, startChunkIndex.intValue(), txn.getFirstLocation().owner,
                            signingPair(), mirrorBatId(), crypto.random, crypto.hasher);
                }).thenApply(v -> new Pair<>(v, Optional.of(new NamedRelativeCapability(txn.targetFilename(), fromParent))));
    }

    private CompletableFuture<Long> findFirstAbsentChunkIndex(long currentIndex, byte[] streamSecret, Location currentLoc, Snapshot s, NetworkAccess network, Crypto crypto) {
        return network.chunkIsPresent(s, currentLoc.owner, currentLoc.writer, currentLoc.getMapKey())
                .thenCompose(present -> present ?
                        FileProperties.calculateNextMapKey(streamSecret, currentLoc.getMapKey(), Optional.empty(), crypto.hasher)
                                .thenCompose(p -> findFirstAbsentChunkIndex(currentIndex + 1, streamSecret, currentLoc.withMapKey(p.left), s, network, crypto)) :
                        Futures.of(currentIndex));
    }

    @JsMethod
    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,
                                                              AsyncReader fileData,
                                                              int fileSizeHi,
                                                              int fileSizeLow,
                                                              NetworkAccess network,
                                                              Crypto crypto,
                                                              ProgressConsumer<Long> monitor) {
        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);
        return uploadOrReplaceFile(filename, fileData, fileSize, network, crypto, monitor,
                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());
    }

    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,
                                                              AsyncReader fileData,
                                                              long length,
                                                              NetworkAccess network,
                                                              Crypto crypto,
                                                              ProgressConsumer<Long> monitor) {
        return uploadOrReplaceFile(filename, fileData, length, network, crypto, monitor,
                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());
    }

    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,
                                                              AsyncReader fileData,
                                                              long length,
                                                              NetworkAccess network,
                                                              Crypto crypto,
                                                              ProgressConsumer<Long> monitor,
                                                              byte[] firstChunkMapKey,
                                                              Optional<Bat> firstChunkBat,
                                                              Optional<BatId> mirrorBat) {
        return uploadFileSection(filename, fileData, false, 0, length, Optional.empty(),
                true, network, crypto, monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat)
                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)
                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto))
                        .thenCompose(c -> f.getUpdated(f.version.mergeAndOverwriteWith(c.version), network)));
    }

    public CompletableFuture<Snapshot> uploadOrReplaceFile(String filename,
                                                           AsyncReader fileData,
                                                           long length,
                                                           boolean isHidden,
                                                           Snapshot s,
                                                           Committer c,
                                                           NetworkAccess network,
                                                           Crypto crypto,
                                                           ProgressConsumer<Long> monitor,
                                                           byte[] firstChunkMapKey,
                                                           Optional<Bat> firstChunkBat,
                                                           Optional<BatId> mirrorBat) {
        return uploadFileSection(s, c, filename, fileData, isHidden, 0, length,
                Optional.empty(), false, true, true, network, crypto,
                monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat);
    }

    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,
                                                              AsyncReader fileData,
                                                              long length,
                                                              boolean isHidden,
                                                              Optional<BatId> mirrorBat,
                                                              NetworkAccess network,
                                                              Crypto crypto) {
        return uploadAndReturnFile(filename, fileData, length, isHidden, x -> {}, mirrorBat, network, crypto);
    }

    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,
                                                              AsyncReader fileData,
                                                              long length,
                                                              boolean isHidden,
                                                              ProgressConsumer<Long> progressMonitor,
                                                              Optional<BatId> mirrorBat,
                                                              NetworkAccess network,
                                                              Crypto crypto) {
        return uploadFileSection(filename, fileData, isHidden, 0, length, Optional.empty(),
                true, network, crypto, progressMonitor, crypto.random.randomBytes(32), Optional.empty(),
                Optional.of(Bat.random(crypto.random)), mirrorBat)
                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)
                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto)));
    }

    @JsMethod
    public CompletableFuture<FileWrapper> overwriteFileJS(AsyncReader fileData,
                                                          int endHigh,
                                                          int endLow,
                                                          NetworkAccess network,
                                                          Crypto crypto,
                                                          ProgressConsumer<Long> monitor) {
        long newSize = LongUtil.intsToLong(endHigh, endLow);
        return overwriteFile(fileData, newSize, network, crypto, monitor);
    }

    public CompletableFuture<Snapshot> overwriteFile(AsyncReader fileData,
                                                     long newSize,
                                                     NetworkAccess network,
                                                     Crypto crypto,
                                                     ProgressConsumer<Long> monitor,
                                                     Snapshot s,
                                                     Committer committer) {
        long size = getSize();
        return clean(s, committer, network, crypto)
                .thenCompose(u -> u.left.overwriteSection(u.right, committer, fileData,
                        0L, newSize, network, crypto, monitor))
                .thenCompose(v -> newSize >= size ?
                        Futures.of(v) :
                        getUpdated(v, network)
                                .thenCompose(f -> f.truncate(v, committer, newSize, network, crypto)));
    }

    public CompletableFuture<FileWrapper> overwriteFile(AsyncReader fileData,
                                                        long newSize,
                                                        NetworkAccess network,
                                                        Crypto crypto,
                                                        ProgressConsumer<Long> monitor) {
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (s, committer) -> overwriteFile(fileData, newSize, network, crypto, monitor, s, committer))
                .thenCompose(v -> getUpdated(v, network));
    }

    @JsMethod
    public CompletableFuture<FileWrapper> overwriteSectionJS(AsyncReader fileData,
                                                             int startHigh,
                                                             int startLow,
                                                             int endHigh,
                                                             int endLow,
                                                             NetworkAccess network,
                                                             Crypto crypto,
                                                             ProgressConsumer<Long> monitor) {
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (s, committer) -> overwriteSection(s, committer, fileData,
                        LongUtil.intsToLong(startHigh, startLow),
                        LongUtil.intsToLong(endHigh, endLow), network, crypto, monitor))
                .thenCompose(v -> getUpdated(v, network));
    }

    public static final class FileUploadProperties {
        public final String filename;
        public final AsyncReader fileData;
        public final long length;
        public final boolean skipExisting;
        public final boolean overwriteExisting;
        public final ProgressConsumer<Long> monitor;

        @JsConstructor
        public FileUploadProperties(String filename,
                                    AsyncReader fileData,
                                    int lengthHi,
                                    int lengthLow,
                                    boolean skipExisting,
                                    boolean overwriteExisting,
                                    ProgressConsumer<Long> monitor) {
            this.filename = filename;
            this.fileData = fileData;
            this.length = (((long)lengthHi) << 32) | (lengthLow & 0xFFFFFFFFL);
            this.skipExisting = skipExisting;
            this.overwriteExisting = overwriteExisting;
            this.monitor = monitor;
        }
    }

    public static class FolderUploadProperties {
        public final List<String> relativePath;
        public final List<FileUploadProperties> files;

        @JsConstructor
        public FolderUploadProperties(List<String> relativePath, List<FileUploadProperties> files) {
            this.relativePath = relativePath;
            this.files = files;
        }

        public Path path() {
            return PathUtil.get(relativePath.stream().collect(Collectors.joining("/")));
        }
    }

    @JsMethod
    public CompletableFuture<FileWrapper> uploadSubtree(Stream<FolderUploadProperties> directories,
                                                        Optional<BatId> mirrorBat,
                                                        NetworkAccess network,
                                                        Crypto crypto,
                                                        TransactionService txns,
                                                        Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,
                                                        Supplier<Boolean> commitWatcher) {
        // only use the supplied mirror BAT if the parent doesn't have a mirror BAT
        Optional<BatId> mirror = mirrorBatId().or(() -> mirrorBat);
        return getPath(network).thenCompose(path ->
                network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                        (s, c) -> {
                            return getUpdated(s, network).thenCompose(us -> Futures.reduceAll(directories, us,
                                    (dir, children) -> dir.getOrMkdirs(children.relativePath, false, mirror, network, crypto, dir.version, c)
                                            .thenCompose(p -> uploadFolder(PathUtil.get(path).resolve(children.path()), p.right,
                                                    children, mirrorBat, txns, resumeFile, commitWatcher, network, crypto, c)
                                                    .thenCompose(v -> dir.getUpdated(v, network))),
                                    (a, b) -> b))
                                    .thenApply(d -> d.version);
                        },
                        commitWatcher
                )).thenCompose(finished -> getUpdated(finished, network));
    }

    public static CompletableFuture<Snapshot> uploadFolder(Path toParent,
                                                           FileWrapper parent,
                                                           FolderUploadProperties children,
                                                           Optional<BatId> mirrorBat,
                                                           TransactionService transactions,
                                                           Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,
                                                           Supplier<Boolean> commitWatcher,
                                                           NetworkAccess network,
                                                           Crypto crypto,
                                                           Committer c) {
        Pair<Snapshot, List<NamedRelativeCapability>> identity = new Pair<>(parent.version, Collections.emptyList());

        // Upload in order of ascending file size
        List<FileUploadProperties> sortedChildren = children.files.stream()
                .sorted(Comparator.comparingLong(a -> a.length))
                .collect(Collectors.toList());
        return Futures.reduceAll(sortedChildren, identity,
                        (p, f) -> {
                            // don't bother with file upload transactions as single chunk uploads are atomic anyway
                            // (nothing to resume or cleanup later in case of failure)
                            if (f.length <= Chunk.MAX_SIZE || transactions == null) // small files or writable public links
                                return parent.uploadFileSection(p.left, c, f.filename, f.fileData, Optional.empty(), false, 0, f.length,
                                                Optional.empty(), Optional.empty(), Optional.empty(), f.skipExisting,
                                                f.overwriteExisting, true, network.disableCommits(), crypto, f.monitor,
                                                crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), mirrorBat)
                                        .thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())))
                                        .thenCompose(r -> {
                                            if (! network.isFull())
                                                return Futures.of(r);
                                            return atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto);
                                        });

                            network.enableCommits();
                            List<FileUploadTransaction> toClose = new ArrayList<>();
                            LocalDateTime now = LocalDateTime.now();
                            return calculateMimeType(f.fileData, f.length, f.filename).thenCompose(mimeType -> {
                                FileProperties props = new FileProperties(f.filename,
                                        false, false, mimeType, f.length,
                                        now, now, false, Optional.empty(), Optional.of(crypto.random.randomBytes(32)));
                                return Transaction.buildFileUploadTransaction(toParent.resolve(f.filename).toString(), f.length,
                                        props, props.streamSecret.get(), SymmetricKey.random(), SymmetricKey.random(),
                                        SymmetricKey.random(), parent.signingPair(), new Location(parent.owner(), parent.writer(),
                                                crypto.random.randomBytes(32)), Optional.of(Bat.random(crypto.random)), crypto.hasher);
                            }).thenCompose(txn -> transactions.open(p.left, c, txn)
                                            .thenCompose(r -> {
                                                if (r.isB()) // we must clear legacy transactions which can't be resumed or ones whose parent has rotated writer
                                                    return (r.b().isLegacy() || ! parent.writer().equals(r.b().writer()) ? Futures.of(false) : resumeFile.apply(r.b()))
                                                            .thenCompose(resume -> {
                                                                if (resume) {
                                                                    toClose.add(r.b());
                                                                    return parent.resumeUpload(r.b(), f.fileData, f.monitor, p.left, c, network, crypto)
                                                                            .thenCompose(res -> f.fileData.reset().thenCompose(resetAgain ->
                                                                                            parent.generateThumbnailAndUpdate(res.left, c, r.b().writeCap(), f.filename, resetAgain,
                                                                                                    network, false, r.b().props.mimeType,
                                                                                                    f.length, r.b().startTime(), r.b().startTime(), Optional.of(r.b().streamSecret()), f.monitor))
                                                                                    .thenApply(s -> new Pair<>(s, res.right)));
                                                                }
                                                                return transactions.close(p.left, c, r.b())
                                                                        .thenCompose(s2 -> transactions.open(s2, c, txn))
                                                                        .thenCompose(r2 -> {
                                                                            if (r2.isB())
                                                                                throw new IllegalStateException("Error uploading file - concurrent upload of same file?");
                                                                            toClose.add(txn);
                                                                            return f.fileData.reset().thenCompose(reset -> parent.uploadFileSection(r2.a(), c, f.filename, reset, Optional.empty(),
                                                                                    false, 0, f.length, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey),
                                                                                    f.skipExisting, f.overwriteExisting, true,
                                                                                    network, crypto, f.monitor, txn.firstMapKey(),
                                                                                    Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat));
                                                                        });
                                                            }).thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())));
                                                toClose.add(txn);
                                                return f.fileData.reset().thenCompose(reset -> parent.uploadFileSection(r.a(), c, f.filename, f.fileData, Optional.empty(), false,
                                                                0, f.length, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey),f.skipExisting, f.overwriteExisting, true,
                                                                network, crypto, f.monitor, txn.firstMapKey(), Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat))
                                                        .thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())));
                                            })
                            ).thenCompose(r -> atomicallyClearTransactionsAndAddToParent(toClose, r.right, parent, transactions, r.left, c, commitWatcher, network, crypto));
                        },
                        (a, b) -> new Pair<>(b.left, Stream.concat(a.right.stream(), b.right.stream()).collect(Collectors.toList())))
                .thenCompose(r -> atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto))
                .thenApply(x -> x.left);
    }

    private static CompletableFuture<Pair<Snapshot, List<NamedRelativeCapability>>> atomicallyClearTransactionsAndAddToParent(
            List<FileUploadTransaction> toClose,
            List<NamedRelativeCapability> childLinks,
            FileWrapper parent,
            TransactionService transactions,
            Snapshot in,
            Committer c,
            Supplier<Boolean> commitWatcher,
            NetworkAccess network,
            Crypto crypto) {
        if (toClose.isEmpty() && childLinks.isEmpty())
            return Futures.of(new Pair<>(in, childLinks));
        return parent.getUpdated(in, network)
                .thenCompose(latest -> latest.addChildPointers(in, c, childLinks, network.disableCommits(), crypto))
                .thenCompose(res -> Futures.reduceAll(toClose, res, (v, f) -> transactions.close(v, c, f), (a, b) -> b))
                .thenApply(s -> {network.enableCommits(); return s;})
                .thenApply(s -> new Pair<>(s, Collections.<NamedRelativeCapability>emptyList()));
    }

    public Optional<BatId> mirrorBatId() {
        return pointer.fileAccess.mirrorBatId();
    }

    public CompletableFuture<Snapshot> overwriteSection(Snapshot current,
                                                        Committer committer,
                                                        AsyncReader fileData,
                                                        long inputStartIndex,
                                                        long endIndex,
                                                        NetworkAccess network,
                                                        Crypto crypto,
                                                        ProgressConsumer<Long> monitor) {
        if (! isWritable())
            return Futures.errored(new IllegalStateException("Unable to modify file without write access!"));
        if (isDirty())
            return Futures.errored(new IllegalStateException("File needs cleaning before modification."));

        FileProperties props = getFileProperties();
        String filename = props.name;
        LOG.info("Overwriting section [" + Long.toHexString(inputStartIndex) + ", " + Long.toHexString(endIndex) + "] of: " + filename);

        Supplier<Location> legacyLocs = () -> new Location(getLocation().owner, getLocation().writer, crypto.random.randomBytes(32));

        SymmetricKey parentParentKey = getPointer().getParentParentKey();
        Location parentLocation = getPointer().getParentCap().getLocation(owner(), writer());
        Optional<Bat> parentBat = getPointer().getParentCap().bat;
        WritableAbsoluteCapability ourCap = writableFilePointer();
        return current.withWriter(owner(), writer(), network)
                .thenCompose(base -> {
                    FileWrapper us = this;
                    final AtomicLong filesSize = new AtomicLong(props.size);
                    return us.getRetriever(crypto.hasher).thenCompose(retriever -> {
                        SymmetricKey baseKey = us.pointer.capability.rBaseKey;
                        CryptreeNode fileAccess = us.pointer.fileAccess;
                        SymmetricKey dataKey = fileAccess.getDataKey(baseKey);

                        List<Long> startIndexes = new ArrayList<>();

                        for (long startIndex = inputStartIndex; startIndex < endIndex; startIndex = startIndex + Chunk.MAX_SIZE - (startIndex % Chunk.MAX_SIZE))
                            startIndexes.add(startIndex);

                        BiFunction<Snapshot, Long, CompletableFuture<Snapshot>> composer = (version, startIndex) -> {
                            MaybeMultihash currentHash = us.pointer.fileAccess.committedHash();
                            return retriever.getChunk(version.get(us.writer()).props, network, crypto, startIndex,
                                    filesSize.get(), ourCap, props.streamSecret, currentHash, monitor)
                                    .thenCompose(currentLocation -> {
                                                CompletableFuture<Optional<Pair<Location, Optional<Bat>>>> locationAt = retriever
                                                        .getMapLabelAt(version.get(us.writer()).props, ourCap,
                                                                props.streamSecret, startIndex + Chunk.MAX_SIZE, crypto.hasher, network)
                                                        .thenApply(x -> x.map(mb -> new Pair<>(getLocation().withMapKey(mb.left), mb.right)));
                                                return locationAt.thenCompose(locationAndBat ->
                                                        CompletableFuture.completedFuture(new Pair<>(currentLocation, locationAndBat)));
                                            }
                                    ).thenCompose(pair -> {

                                        if (!pair.left.isPresent()) {
                                            CompletableFuture<Snapshot> result = new CompletableFuture<>();
                                            result.completeExceptionally(new IllegalStateException("Current chunk not present"));
                                            return result;
                                        }

                                        LocatedChunk currentOriginal = pair.left.get();
                                        Optional<Pair<Location, Optional<Bat>>> nextChunkLocationOpt = pair.right;
                                        CompletableFuture<Pair<Location, Optional<Bat>>> nextChunkLocationFut = nextChunkLocationOpt
                                                .map(Futures::of)
                                                .orElseGet(() -> props.streamSecret
                                                        .map(streamSecret -> FileProperties.calculateNextMapKey(streamSecret,
                                                                currentOriginal.location.getMapKey(), currentOriginal.bat, crypto.hasher)
                                                                .thenApply(nextMapKeyAndBat -> new Pair<>(us.getLocation().withMapKey(nextMapKeyAndBat.left), nextMapKeyAndBat.right)))
                                                        .orElseGet(() -> Futures.of(new Pair<>(legacyLocs.get(), Optional.empty()))));
                                        return nextChunkLocationFut.thenCompose(nextChunkLocationAndBat -> {
                                            Location nextChunkLocation = nextChunkLocationAndBat.left;
                                            Optional<Bat> nextChunkBat = nextChunkLocationAndBat.right;
                                            LOG.info("********** 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;
                                            Optional<SymmetricLinkToSigner> writerLink = startIndex < Chunk.MAX_SIZE ?
                                                    us.pointer.fileAccess.getWriterLink(us.pointer.capability.rBaseKey) :
                                                    Optional.empty();

                                            return fileData.readIntoArray(raw, internalStart, internalEnd - internalStart).thenCompose(read -> {

                                                Chunk updated = new Chunk(raw, dataKey, currentOriginal.location.getMapKey(), dataKey.createNonce());
                                                LocatedChunk located = new LocatedChunk(currentOriginal.location, currentOriginal.bat, currentOriginal.existingHash, updated);
                                                long currentSize = filesSize.get();
                                                FileProperties newProps = new FileProperties(props.name, false,
                                                        props.isLink, props.mimeType,
                                                        endIndex > currentSize ? endIndex : currentSize,
                                                        LocalDateTime.now(), props.created, props.isHidden,
                                                        props.thumbnail, props.streamSecret);

                                                CompletableFuture<Snapshot> chunkUploaded = FileUploader.uploadChunk(version, committer, us.signingPair(),
                                                        newProps, parentLocation, parentBat, parentParentKey, baseKey, located,
                                                        nextChunkLocation, nextChunkBat, writerLink, mirrorBatId(),
                                                        crypto.random, crypto.hasher, network, monitor);

                                                return chunkUploaded.thenCompose(updatedBase -> {
                                                    //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
                                                            return network.getFile(updatedBase, ourCap, entryWriter, ownername)
                                                                    .thenCompose(updatedUs -> {
                                                                        FileProperties correctedSize = updatedUs.get()
                                                                                .getPointer().fileAccess.getProperties(ourCap.rBaseKey)
                                                                                .withSize(endIndex);
                                                                        return updatedUs.get()
                                                                                .getPointer().fileAccess.updateProperties(updatedBase,
                                                                                        committer, ourCap, entryWriter, correctedSize, network);
                                                                    });
                                                        }
                                                    }
                                                    return CompletableFuture.completedFuture(updatedBase);
                                                });
                                            });
                                        });
                                    });
                        };

                        return Futures.reduceAll(startIndexes, base, composer, (a, b) -> b);
                    });
                });
    }

    public CompletableFuture<FileWrapper> uploadFileSection(String filename,
                                                            AsyncReader fileData,
                                                            boolean isHidden,
                                                            long startIndex,
                                                            long endIndex,
                                                            Optional<SymmetricKey> baseKey,
                                                            boolean overwriteExisting,
                                                            NetworkAccess network,
                                                            Crypto crypto,
                                                            ProgressConsumer<Long> monitor) {
        return uploadFileSection(filename, fileData, isHidden, startIndex, endIndex, baseKey, overwriteExisting,
                network, crypto, monitor, crypto.random.randomBytes(32), Optional.empty(),
                Optional.of(Bat.random(crypto.random)), mirrorBatId());
    }

    /**
     *
     * @param filename
     * @param fileData
     * @param isHidden
     * @param startIndex
     * @param endIndex
     * @param baseKey The desired base key for the uploaded file. If absent a random key is generated.
     * @param overwriteExisting
     * @param network
     * @param crypto
     * @param monitor A way to report back progress in number of bytes of file written
     * @param firstChunkMapKey The planned location for the first chunk
     * @return The updated version of this directory after the upload
     */
    public CompletableFuture<FileWrapper> uploadFileSection(String filename,
                                                            AsyncReader fileData,
                                                            boolean isHidden,
                                                            long startIndex,
                                                            long endIndex,
                                                            Optional<SymmetricKey> baseKey,
                                                            boolean overwriteExisting,
                                                            NetworkAccess network,
                                                            Crypto crypto,
                                                            ProgressConsumer<Long> monitor,
                                                            byte[] firstChunkMapKey,
                                                            Optional<byte[]> streamSecret,
                                                            Optional<Bat> firstBat,
                                                            Optional<BatId> mirrorBat) {
        if (isWritable())
            return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->
                    uploadFileSection(current, committer, filename, fileData, isHidden, startIndex, endIndex,
                            baseKey, false, overwriteExisting, false, network, crypto, monitor, firstChunkMapKey, streamSecret, firstBat, mirrorBat))
                    .thenCompose(finalBase -> getUpdated(finalBase, network));

        if (! overwriteExisting)
            return Futures.errored(new IllegalStateException("Cannot upload a file to a directory without write access!"));
        return getChild(filename, crypto.hasher, network)
                .thenCompose(c -> {
                    if (! c.isPresent())
                        return Futures.errored(new IllegalStateException("No child with name " + filename));
                    FileWrapper child = c.get();
                    return network.synchronizer.applyComplexUpdate(owner(), child.signingPair(),
                            (current, committer) -> updateExistingChild(current, committer, child,
                                    fileData, startIndex, endIndex, network, crypto, monitor))
                            .thenApply(childVersion -> withVersion(version.mergeAndOverwriteWith(childVersion)));
                });
    }

    private CompletableFuture<Snapshot> updateSize(Committer committer,
                                                   long newSize,
                                                   NetworkAccess network) {
        FileProperties newProps = getFileProperties().withSize(newSize);
        return updateProperties(version, committer, newProps, network);
    }

    @JsMethod
    public CompletableFuture<FileWrapper> updateThumbnail(String base64Str, NetworkAccess network) {
        Optional<Thumbnail> thumbData = Optional.empty();
        if (base64Str != null && base64Str.length() > 0) {
            thumbData = convertFromBase64(base64Str);
        }
        FileProperties updatedProperties = this.props.withThumbnail(thumbData);
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (s, committer) -> updateProperties(s, committer, updatedProperties, network)
        ).thenCompose(finished -> getUpdated(finished, network));
    }

    public CompletableFuture<Snapshot> uploadFileSection(Snapshot initialVersion,
                                                         Committer committer,
                                                         String filename,
                                                         AsyncReader fileData,
                                                         boolean isHidden,
                                                         long startIndex,
                                                         long endIndex,
                                                         Optional<SymmetricKey> baseKey,
                                                         boolean skipExisting,
                                                         boolean overwriteExisting,
                                                         boolean truncateExisting,
                                                         NetworkAccess network,
                                                         Crypto crypto,
                                                         ProgressConsumer<Long> monitor,
                                                         byte[] firstChunkMapKey,
                                                         Optional<byte[]> streamSecret,
                                                         Optional<Bat> firstBat,
                                                         Optional<BatId> requestedMirrorBat) {
        return uploadFileSection(initialVersion, committer, filename, fileData, Optional.empty(), isHidden, startIndex, endIndex,
                baseKey, Optional.empty(), Optional.empty(), skipExisting, overwriteExisting, truncateExisting, network, crypto, monitor, firstChunkMapKey, streamSecret, firstBat, requestedMirrorBat)
                .thenCompose(p -> getUpdated(p.left, network)
                        .thenCompose(latest -> p.right.isEmpty() ?
                                Futures.of(p.left) :
                                latest.addChildPointer(p.left, committer, p.right.get(), network, crypto)));
    }

    private CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> uploadFileSection(
            Snapshot initialVersion,
            Committer committer,
            String filename,
            AsyncReader fileData,
            Optional<Thumbnail> existingThumbnail,
            boolean isHidden,
            long startIndex,
            long endIndex,
            Optional<SymmetricKey> requestedBaseKey,
            Optional<SymmetricKey> requestedDataKey,
            Optional<SymmetricKey> requestedWriteKey,
            boolean skipExisting,
            boolean overwriteExisting,
            boolean truncateExisting,
            NetworkAccess network,
            Crypto crypto,
            ProgressConsumer<Long> monitor,
            byte[] firstChunkMapKey,
            Optional<byte[]> streamSecret,
            Optional<Bat> firstBat,
            Optional<BatId> requestedMirrorBat) {
        if (!isLegalName(filename)) {
            return Futures.errored(new IllegalStateException("Illegal filename: " + filename));
        }
        if (! isDirectory()) {
            return Futures.errored(new IllegalStateException("Cannot upload a sub file to a file!"));
        }
        Optional<BatId> mirrorBat = mirrorBatId().or(() -> requestedMirrorBat);
        return initialVersion.withWriter(owner(), writer(), network)
                .thenCompose(current -> getUpdated(current, network)
                        .thenCompose(latest -> latest.getChild(current, filename, network)
                                        .thenCompose(childOpt -> {
                                            if (childOpt.isPresent()) {
                                                if (skipExisting)
                                                    return Futures.of(new Pair<>(current, Optional.empty()));
                                                if (! overwriteExisting)
                                                    throw new FileExistsException(filename);
                                                FileWrapper child = childOpt.get();
                                                FileProperties childProps = child.getFileProperties();

                                                TriFunction<FileWrapper, Snapshot, Long, CompletableFuture<Snapshot>> updatePropsIfNecessary =
                                                        (updatedChild, latestSnapshot, writeEnd) -> {
                                                    if (childProps.thumbnail.isEmpty()) {
                                                        if (writeEnd <= childProps.size)
                                                            return Futures.of(latestSnapshot);
                                                        // update size only
                                                        return updatedChild.updateSize(committer, writeEnd, network);
                                                    }
                                                    return updatedChild.getInputStream(latestSnapshot.get(updatedChild.writer()).props, network, crypto, l -> {})
                                                            .thenCompose(is -> updatedChild.recalculateThumbnail(
                                                                latestSnapshot, committer, filename, is, isHidden,
                                                                updatedChild.getSize(), updatedChild.props.created, network, (WritableAbsoluteCapability)updatedChild.pointer.capability,
                                                                updatedChild.getFileProperties().streamSecret));
                                                };

                                                if (truncateExisting && endIndex < childProps.size) {
                                                    return child.truncate(current, committer, endIndex, network, crypto).thenCompose( updatedSnapshot ->
                                                        getUpdated(updatedSnapshot, network).thenCompose( updatedParent ->
                                                                child.getUpdated(updatedSnapshot, network).thenCompose( updatedChild ->
                                                                    updatedParent.updateExistingChild(updatedSnapshot, committer, updatedChild, fileData,
                                                                        startIndex, endIndex, network, crypto, monitor)
                                                                            .thenCompose(latestSnapshot ->  updatePropsIfNecessary.apply(updatedChild, latestSnapshot, endIndex)))))
                                                            .thenApply(s -> new Pair<>(s, Optional.<NamedRelativeCapability>empty()));
                                                } else {
                                                    return updateExistingChild(current, committer, child, fileData,
                                                            startIndex, endIndex, network, crypto, monitor)
                                                            .thenApply(s -> {
                                                                monitor.accept(THUMBNAIL_PROGRESS_OFFSET);
                                                                return new Pair<>(s, Optional.<NamedRelativeCapability>empty());
                                                            });
                                                }
                                            }
                                            if (startIndex > 0) {
                                                // TODO if startIndex > 0 prepend with a zero section
                                                throw new IllegalStateException("Unimplemented!");
                                            }
                                            SymmetricKey fileWriteKey = requestedWriteKey.orElseGet(SymmetricKey::random);
                                            SymmetricKey fileKey = requestedBaseKey.orElseGet(SymmetricKey::random);
                                            SymmetricKey dataKey = requestedDataKey.orElseGet(SymmetricKey::random);
                                            SymmetricKey rootRKey = latest.pointer.capability.rBaseKey;
                                            CryptreeNode dirAccess = latest.pointer.fileAccess;
                                            SymmetricKey dirParentKey = dirAccess.getParentKey(rootRKey);
                                            Location parentLocation = getLocation();
                                            Optional<Bat> parentBat = writableFilePointer().bat;
                                            LocalDateTime timestamp = LocalDateTime.now();
                                            return calculateMimeType(fileData, endIndex, filename).thenCompose(mimeType -> fileData.reset()
                                                    .thenCompose(resetReader -> {
                                                        Optional<byte[]> actualStreamSecret = streamSecret.isPresent() ?
                                                                streamSecret :
                                                                Optional.of(crypto.random.randomBytes(32));
                                                        FileProperties fileProps = new FileProperties(filename,
                                                                false, false, mimeType, endIndex,
                                                                timestamp, timestamp, isHidden, existingThumbnail, actualStreamSecret);

                                                        FileUploader chunks = new FileUploader(filename, resetReader,
                                                                startIndex, endIndex, fileKey, dataKey, parentLocation, parentBat,
                                                                dirParentKey, monitor, fileProps, firstChunkMapKey, firstBat);

                                                        SigningPrivateKeyAndPublicHash signer = signingPair();
                                                        WritableAbsoluteCapability fileWriteCap = new
                                                                WritableAbsoluteCapability(owner(),
                                                                signer.publicKeyHash,
                                                                firstChunkMapKey, firstBat, fileKey,
                                                                fileWriteKey);

                                                        return chunks.upload(current, committer, network, parentLocation.owner,
                                                                signer, mirrorBat, crypto.random, crypto.hasher)
                                                                .thenCompose(cwd -> fileData.reset().thenCompose(resetAgain ->
                                                                        generateThumbnailAndUpdate(cwd, committer, fileWriteCap, filename, resetAgain,
                                                                                network, isHidden, mimeType,
                                                                                endIndex, timestamp, timestamp, actualStreamSecret, monitor)))
                                                                .thenApply(s -> new Pair<>(s, Optional.of(new NamedRelativeCapability(filename, writableFilePointer().relativise(fileWriteCap)))));
                                                    }));
                                        })
                        )
                );
    }

    @JsMethod
    public CompletableFuture<Boolean> calculateAndUpdateThumbnail(NetworkAccess network, Crypto crypto) {
        return network.synchronizer.applyComplexComputation(owner(), signingPair(),
                (latestSnapshot, committer) -> getInputStream(latestSnapshot.get(writer()).props, network, crypto, l -> {})
                        .thenCompose(is -> recalculateThumbnail(
                                latestSnapshot, committer, getName(), is, props.isHidden,
                                getSize(), props.created, network, (WritableAbsoluteCapability)pointer.capability,
                                getFileProperties().streamSecret))
                        .thenApply(res -> new Pair<>(res, true))
                        .exceptionally(ex -> new Pair<>(latestSnapshot, false))
        ).thenApply(p -> p.right);
    }

    private CompletableFuture<Snapshot> recalculateThumbnail(Snapshot snapshot, Committer committer, String filename,
                                                             AsyncReader fileData, boolean isHidden, long fileSize,
                                                             LocalDateTime createdDateTime, NetworkAccess network,
                                                             WritableAbsoluteCapability fileWriteCap, Optional<byte[]> streamSecret
    ) {
        return fileData.reset()
                .thenCompose(fileData2 -> calculateMimeType(fileData2, fileSize, filename)
                        .thenCompose(mimeType -> fileData.reset()
                                .thenCompose(resetAgain ->
                                    generateThumbnailAndUpdate(snapshot, committer, fileWriteCap, filename, resetAgain,
                                            network, isHidden, mimeType, fileSize, LocalDateTime.now(), createdDateTime, streamSecret, true, x -> {}))));
    }

    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,
                                                                   Committer committer,
                                                                   WritableAbsoluteCapability cap,
                                                                   String fileName,
                                                                   AsyncReader fileData,
                                                                   NetworkAccess network,
                                                                   Boolean isHidden,
                                                                   String mimeType,
                                                                   long fileSize,
                                                                   LocalDateTime updatedDateTime,
                                                                   LocalDateTime createdDateTime,
                                                                   Optional<byte[]> streamSecret,
                                                                   ProgressConsumer<Long> monitor) {
        return generateThumbnailAndUpdate(base, committer, cap, fileName, fileData, network, isHidden,
                mimeType, fileSize, updatedDateTime, createdDateTime, streamSecret, false, monitor);
    }

    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,
                                                                   Committer committer,
                                                                   WritableAbsoluteCapability cap,
                                                                   String fileName,
                                                                   AsyncReader fileData,
                                                                   NetworkAccess network,
                                                                   Boolean isHidden,
                                                                   String mimeType,
                                                                   long fileSize,
                                                                   LocalDateTime updatedDateTime,
                                                                   LocalDateTime createdDateTime,
                                                                   Optional<byte[]> streamSecret,
                                                                   boolean replaceExistingThumbnail,
                                                                   ProgressConsumer<Long> monitor) {
        return network.getFile(base, cap, getChildsEntryWriter(), ownername).thenCompose(fileOpt -> {
            if (replaceExistingThumbnail || fileOpt.get().props.thumbnail.isEmpty()) {
                return generateThumbnail(network, fileData, (int) Math.min(fileSize, Integer.MAX_VALUE), fileName, mimeType)
                        .thenCompose(thumbData -> {
                            if (thumbData.isEmpty())
                                return Futures.of(base);
                            FileProperties fileProps = new FileProperties(fileName, false, props.isLink, mimeType, fileSize,
                                    updatedDateTime, createdDateTime, isHidden, thumbData, streamSecret);

                            return fileOpt.get().updateProperties(base, committer, fileProps, network);
                        });
            } else {
                return Futures.of(base);
            }
        }).thenApply(s -> {
            monitor.accept(THUMBNAIL_PROGRESS_OFFSET);
            return s;
        });
    }

    private CompletableFuture<Snapshot> updateProperties(Snapshot base,
                                                         Committer committer,
                                                         FileProperties newProps,
                                                         NetworkAccess network) {
        return getPointer().fileAccess.updateProperties(base, committer, writableFilePointer(),
                getChildsEntryWriter(), newProps, network);
    }

    private CompletableFuture<Snapshot> addChildPointer(Snapshot current,
                                                        Committer committer,
                                                        NamedRelativeCapability childCap,
                                                        NetworkAccess network,
                                                        Crypto crypto) {
        return addChildPointers(current, committer, Collections.singletonList(childCap), network, crypto);
    }

    private CompletableFuture<Snapshot> addChildPointers(Snapshot current,
                                                        Committer committer,
                                                        List<NamedRelativeCapability> childCaps,
                                                        NetworkAccess network,
                                                        Crypto crypto) {
        return pointer.fileAccess.addChildrenAndCommit(current, committer,
                childCaps, writableFilePointer(), signingPair(), mirrorBatId(), network, crypto)
                .thenApply(newBase -> {
                    setModified();
                    return newBase;
                });
    }

    public CompletableFuture<FileWrapper> appendToChild(String filename,
                                                        long expectedSize,
                                                        byte[] fileData,
                                                        boolean isHidden,
                                                        Optional<BatId> mirrorBat,
                                                        NetworkAccess network,
                                                        Crypto crypto,
                                                        ProgressConsumer<Long> monitor) {
        return getChild(filename, crypto.hasher, network)
                .thenCompose(child -> child
                        .flatMap(c -> c.getFileProperties().streamSecret)
                        .map(secret -> FileProperties.calculateMapKey(secret,
                                child.get().getLocation().getMapKey(),
                                child.get().pointer.capability.bat,
                                child.get().getFileProperties().size, crypto.hasher)
                                .thenApply(p -> new Triple<>(p.left, p.right, Optional.of(secret))))
                        .orElseGet(() -> Futures.of(new Triple<>(crypto.random.randomBytes(32),
                                Optional.of(Bat.random(crypto.random)), Optional.empty())))
                        .thenCompose(x -> {
                            long size = child.map(f -> f.getSize()).orElse(0L);
                            if (size != expectedSize)
                                throw new IllegalStateException("File has been concurrently modified!");
                            return uploadFileSection(filename, AsyncReader.build(fileData), isHidden,
                                    size,
                                    fileData.length + size,
                                    child.map(f -> f.getPointer().capability.rBaseKey), true, network, crypto,
                                    monitor, x.left, x.right, x.middle, mirrorBat);
                        }));
    }

    public CompletableFuture<Snapshot> append(byte[] fileData,
                                              NetworkAccess network,
                                              Crypto crypto,
                                              Committer committer,
                                              ProgressConsumer<Long> progress) {
        long size = getSize();
        return overwriteSection(version, committer, AsyncReader.build(fileData), size, size + fileData.length, network, crypto, progress);
    }

    /**
     *
     * @param current
     * @param committer
     * @param existingChild
     * @param fileData
     * @param inputStartIndex
     * @param endIndex
     * @param network
     * @param crypto
     * @param monitor
     * @return The committed root for the parent (this) directory
     */
    private CompletableFuture<Snapshot> updateExistingChild(Snapshot current,
                                                            Committer committer,
                                                            FileWrapper existingChild,
                                                            AsyncReader fileData,
                                                            long inputStartIndex,
                                                            long endIndex,
                                                            NetworkAccess network,
                                                            Crypto crypto,
                                                            ProgressConsumer<Long> monitor) {

        FileProperties existingProps = existingChild.getFileProperties();
        String filename = existingProps.name;
        LOG.info("Overwriting section [" + Long.toHexString(inputStartIndex) + ", " + Long.toHexString(endIndex) + "] of child with name: " + filename);

        return current.withWriter(existingChild.owner(), existingChild.writer(), network)
                .thenCompose(state ->
                        existingChild.clean(state, committer, network, crypto)
                                .thenCompose(pair -> pair.left.overwriteSection(pair.right, committer, fileData,
                        inputStartIndex, endIndex, network, crypto, monitor)));
    }

    static boolean isLegalName(String name) {
        return !name.contains("/") && ! name.equals(".") && ! name.equals("..") && ! name.isEmpty();
    }

    /**
     *
     * @param newFolderName
     * @param network
     * @param isSystemFolder
     * @param crypto
     * @return An updated version of this directory
     */
    @JsMethod
    public CompletableFuture<FileWrapper> mkdir(String newFolderName,
                                                NetworkAccess network,
                                                boolean isSystemFolder,
                                                Optional<BatId> mirrorBat,
                                                Crypto crypto) {
        return mkdir(newFolderName, network, null, Optional.empty(), isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto);
    }

    public CompletableFuture<FileWrapper> mkdir(String newFolderName,
                                                NetworkAccess network,
                                                SymmetricKey requestedBaseSymmetricKey,
                                                Optional<Bat> desiredBat,
                                                boolean isSystemFolder,
                                                Optional<BatId> mirrorBat,
                                                Crypto crypto) {

        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (state, committer) -> mkdir(newFolderName, Optional.ofNullable(requestedBaseSymmetricKey),
                        Optional.empty(), Optional.empty(), desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), network, crypto, state, committer))
                .thenCompose(version -> getUpdated(version, network));
    }

    public CompletableFuture<Snapshot> mkdir(String newFolderName,
                                             Optional<SymmetricKey> requestedBaseReadKey,
                                             Optional<SymmetricKey> requestedBaseWriteKey,
                                             Optional<byte[]> desiredMapKey,
                                             Optional<Bat> desiredBat,
                                             boolean isSystemFolder,
                                             Optional<BatId> mirrorBat,
                                             NetworkAccess network,
                                             Crypto crypto,
                                             Snapshot version,
                                             Committer committer) {

        if (!this.isDirectory()) {
            return Futures.errored(new IllegalStateException("Cannot mkdir in a file!"));
        }
        if (!isLegalName(newFolderName)) {
            return Futures.errored(new IllegalStateException("Illegal directory name: " + newFolderName));
        }
        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);
        return hasChildWithName(fullVersion, newFolderName, crypto.hasher, network).thenCompose(hasChild -> {
            if (hasChild) {
                return Futures.errored(new IllegalStateException("Child already exists with name: " + newFolderName));
            }
            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),
                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto).thenApply(x -> {
                setModified();
                return x;
            });
        });
    }

    /** Get or create a descendant directory
     *
     * @param subPath
     * @param network
     * @param isSystemFolder
     * @param crypto
     * @return
     */
    @JsMethod
    public CompletableFuture<FileWrapper> getOrMkdirs(Path subPath,
                                                      NetworkAccess network,
                                                      boolean isSystemFolder,
                                                      Optional<BatId> mirrorBat,
                                                      Crypto crypto) {
        String finalPath = TrieNode.canonicalise(subPath.toString());
        List<String> elements = Arrays.asList(finalPath.split("/"));
        return network.synchronizer.applyComplexComputation(owner(), signingPair(),
                (state, committer) -> getOrMkdirs(elements, isSystemFolder, mirrorBat, network, crypto, state, committer))
                .thenApply(p -> p.right);
    }

    public CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdirs(List<String> subPath,
                                                                      boolean isSystemFolder,
                                                                      Optional<BatId> mirrorBat,
                                                                      NetworkAccess network,
                                                                      Crypto crypto,
                                                                      Snapshot version,
                                                                      Committer committer) {
        return Futures.reduceAll(subPath, new Pair<>(version, this),
                (p, name) -> p.right.getOrMkdir(name, Optional.empty(), Optional.empty(), Optional.empty(),
                                Optional.empty(), isSystemFolder, p.right.mirrorBatId().or(() -> mirrorBat), network, crypto, p.left, committer),
                (a, b) -> b);
    }

    private CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdir(String newFolderName,
                                                                      Optional<SymmetricKey> requestedBaseReadKey,
                                                                      Optional<SymmetricKey> requestedBaseWriteKey,
                                                                      Optional<byte[]> desiredMapKey,
                                                                      Optional<Bat> desiredBat,
                                                                      boolean isSystemFolder,
                                                                      Optional<BatId> mirrorBat,
                                                                      NetworkAccess network,
                                                                      Crypto crypto,
                                                                      Snapshot version,
                                                                      Committer committer) {

        if (! this.isDirectory()) {
            return Futures.errored(new IllegalStateException("Cannot mkdir in a file!"));
        }
        if (! isLegalName(newFolderName)) {
            return Futures.errored(new IllegalStateException("Illegal directory name: " + newFolderName));
        }
        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);
        return getChild(fullVersion, newFolderName, network).thenCompose(childOpt -> {
            if (childOpt.isPresent()) {
                return Futures.of(new Pair<>(fullVersion, childOpt.get()));
            }
            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),
                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBat, crypto).thenCompose(x -> {
                setModified();
                return getUpdated(x, network).thenCompose(us -> us.getChild(newFolderName, crypto.hasher, network))
                        .thenApply(child -> new Pair<>(x, child.get()));
            });
        });
    }

    /**
     * @param newFilename
     * @param parent
     * @param userContext
     * @return the updated parent
     */
    @JsMethod
    public CompletableFuture<FileWrapper> rename(String newFilename,
                                                 FileWrapper parent,
                                                 Path ourPath,
                                                 UserContext userContext) {
        if (! isLegalName(newFilename))
            return CompletableFuture.completedFuture(parent);
        if (! parent.isWritable())
            return Futures.errored(new IllegalStateException("Unable to rename something without write access to the parent!"));
        CompletableFuture<Optional<FileWrapper>> childExists = parent == null ?
                CompletableFuture.completedFuture(Optional.empty()) :
                parent.getDescendentByPath(newFilename, userContext.crypto.hasher, userContext.network);
        ensureUnmodified();
        FileProperties currentProps = getFileProperties();
        setModified();
        return childExists
                .thenCompose(existing -> {
                    if (existing.isPresent())
                        throw new IllegalStateException("Cannot rename, child already exists with name: " + newFilename);

                    //get current props
                    RetrievedCapability ourPointer = linkPointer.orElse(pointer);
                    WritableAbsoluteCapability us = (WritableAbsoluteCapability) ourPointer.capability;
                    CryptreeNode nodeToUpdate = ourPointer.fileAccess;

                    boolean isDir = this.isDirectory();
                    boolean isLink = ourPointer.getProperties().isLink;
                    FileProperties newProps = new FileProperties(newFilename, isDir, isLink,
                            currentProps.mimeType, currentProps.size,
                            currentProps.modified, currentProps.created, currentProps.isHidden,
                            currentProps.thumbnail, currentProps.streamSecret);
                    SigningPrivateKeyAndPublicHash signer = isLink ? parent.signingPair() : signingPair();
                    return userContext.network.synchronizer.applyComplexUpdate(owner(), signer,
                            (s, committer) -> nodeToUpdate.updateProperties(s, committer, us,
                                    entryWriter, newProps, userContext.network)
                                    .thenCompose(updated -> parent.updateChildLinks(updated, committer,
                                            Arrays.asList(new Pair<>(us, new NamedAbsoluteCapability(newFilename, us))),
                                            userContext.network, userContext.crypto.random, userContext.crypto.hasher))
                                    .thenCompose(v -> userContext.isSecretLink() ? Futures.of(v) :
                                            userContext.sharedWithCache.rename(ourPath,
                                                    ourPath.getParent().resolve(newFilename), v, committer, userContext.network))
                    ).thenCompose(newVersion -> parent.getUpdated(newVersion, userContext.network));
                });
    }

    public CompletableFuture<Boolean> setProperties(FileProperties updatedProperties,
                                                    Hasher hasher,
                                                    NetworkAccess network,
                                                    Optional<FileWrapper> parent) {
        setModified();
        String newName = updatedProperties.name;
        if (!isLegalName(newName)) {
            return Futures.errored(new IllegalArgumentException("Illegal file name: " + newName));
        }
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (s, comitter) -> (! parent.isPresent() ?
                        CompletableFuture.completedFuture(s) :
                        s.withWriter(owner(), parent.get().writer(), network)
                ).thenCompose(withParent -> parent.get().hasChildWithName(withParent, newName, hasher, network))
                        .thenCompose(hasChild -> ! hasChild ?
                                CompletableFuture.completedFuture(true) :
                                parent.get().getChildrenCapabilities(hasher, network)
                                        .thenApply(childCaps -> {
                                            if (! childCaps.stream()
                                                    .map(l -> new ByteArrayWrapper(l.cap.getMapKey()))
                                                    .collect(Collectors.toSet())
                                                    .contains(new ByteArrayWrapper(pointer.capability.getMapKey())))
                                                throw new IllegalStateException("Cannot rename to same name as an existing file");
                                            return true;
                                        })).thenCompose(x -> {
                            CryptreeNode fileAccess = pointer.fileAccess;
                            return fileAccess.updateProperties(s, comitter, writableFilePointer(),
                                    entryWriter, updatedProperties, network);
                        }))
                .thenApply(fa -> true);
    }

    @JsMethod
    public AbsoluteCapability readOnlyPointer() {
        return pointer.capability.readOnly();
    }

    public WritableAbsoluteCapability writableFilePointer() {
        if (! isWritable())
            throw new IllegalStateException("File is not writable!");
        return (WritableAbsoluteCapability) pointer.capability;
    }

    public SigningPrivateKeyAndPublicHash signingPair() {
        if (! isWritable())
            throw new IllegalStateException("File is not writable!");
        return pointer.capability.wBaseKey
                .map(w -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, w, entryWriter))
                .orElseGet(entryWriter::get);
    }

    @JsMethod
    public CompletableFuture<Boolean> moveTo(FileWrapper target, FileWrapper parent, Path ourPath, UserContext context) {
        return copyTo(target, context)
                .thenCompose(fw -> remove(parent, ourPath, context))
                .thenApply(newAccess -> true);
    }

    @JsMethod
    public CompletableFuture<Boolean> copyTo(FileWrapper target, UserContext context) {
        ensureUnmodified();
        NetworkAccess network = context.network;
        Crypto crypto = context.crypto;
        if (! target.isDirectory()) {
            return Futures.errored(new IllegalStateException("CopyTo target " + target + " must be a directory"));
        }

        return context.network.synchronizer.applyComplexUpdate(target.owner(), target.signingPair(),
                (version, committer) -> version.withWriter(owner(), writer(), network)
                        .thenCompose(both -> copyTo(target, this.props.thumbnail, network, crypto, both, committer)))
                .thenApply(newAccess -> true);
    }

    private CompletableFuture<Snapshot> copyTo(FileWrapper target,
                                              Optional<Thumbnail> existingThumbnail,
                                              NetworkAccess network,
                                              Crypto crypto,
                                              Snapshot version,
                                              Committer committer) {
        if (! target.isDirectory()) {
            return Futures.errored(new IllegalStateException("CopyTo target " + target + " must be a directory"));
        }

        return target.hasChildWithName(version, getFileProperties().name, crypto.hasher, network).thenCompose(childExists -> {
            if (childExists) {
                return Futures.errored(new IllegalStateException("CopyTo target " + target + " already has child with name " + getFileProperties().name));
            }
            if (isDirectory()) {
                byte[] newMapKey = crypto.random.randomBytes(32);
                Optional<Bat> newBat = Optional.of(Bat.random(crypto.random));
                SymmetricKey newBaseR = SymmetricKey.random();
                SymmetricKey newBaseW = SymmetricKey.random();
                WritableAbsoluteCapability newCap = ((WritableAbsoluteCapability)target.getPointer().capability)
                        .withMapKey(newMapKey, newBat)
                        .withBaseKey(newBaseR)
                        .withBaseWriteKey(newBaseW);
                return withVersion(this.version.mergeAndOverwriteWith(version))
                        .getChildren(version, crypto.hasher, network).thenCompose(children ->
                        target.mkdir(getName(), Optional.of(newBaseR), Optional.of(newBaseW), Optional.of(newMapKey),
                                newBat, getFileProperties().isHidden, target.mirrorBatId(), network, crypto, version, committer)
                                .thenCompose(versionWithDir ->
                                        network.getFile(versionWithDir, newCap, target.getChildsEntryWriter(), target.ownername)
                                                .thenCompose(subTargetOpt -> {
                                                    FileWrapper newTarget = subTargetOpt.get();
                                                    return Futures.reduceAll(children, versionWithDir,
                                                            (s, child) -> newTarget.getUpdated(s, network)
                                                                    .thenCompose(updated ->
                                                                            child.copyTo(updated, existingThumbnail, network, crypto, s, committer)),
                                                            (a, b) -> a.merge(b));
                                                })));
            } else {
                return version.withWriter(owner(), writer(), network).thenCompose(snapshot ->
                        getInputStream(snapshot.get(writer()).props, network, crypto, x -> {})
                                .thenCompose(stream -> target.uploadFileSection(snapshot, committer,
                                                getName(), stream, existingThumbnail, false, 0, getSize(),
                                                Optional.empty(), Optional.empty(), Optional.empty(), false, false, false, network, crypto, x -> {},
                                                crypto.random.randomBytes(32), Optional.empty(),
                                                Optional.of(Bat.random(crypto.random)), target.mirrorBatId())
                                        .thenCompose(p -> p.right.isEmpty() ?
                                                Futures.of(p.left) :
                                                target.addChildPointer(p.left, committer, p.right.get(), network, crypto))));
            }
        });
    }

    @JsMethod
    public CompletableFuture<Boolean> hasChild(String fileName, Hasher hasher, NetworkAccess network) {
        if (!isLegalName(fileName)) {
            return Futures.errored(new IllegalArgumentException("Illegal file/directory name: " + fileName));
        }
        return this.hasChildWithName(version, fileName, hasher, network).thenApply(childExists -> childExists);
    }

    /**
     * Move this file/dir and subtree to a new signing key pair.
     * @param signer
     * @param parent
     * @param network
     * @return The updated version of this file/dir and its parent
     */
    public CompletableFuture<Pair<FileWrapper, FileWrapper>> changeSigningKey(SigningPrivateKeyAndPublicHash signer,
                                                                              FileWrapper parent,
                                                                              NetworkAccess network,
                                                                              SafeRandom random,
                                                                              Hasher hasher) {
        ensureUnmodified();
        WritableAbsoluteCapability cap = (WritableAbsoluteCapability)getPointer().capability;
        SymmetricLinkToSigner signerLink = SymmetricLinkToSigner.fromPair(cap.wBaseKey.get(), signer);
        CryptreeNode fileAccess = getPointer().fileAccess;

        RelativeCapability newParentLink = new RelativeCapability(Optional.of(parent.writer()),
                parent.getLocation().getMapKey(), parent.writableFilePointer().bat, parent.getParentKey(), Optional.empty());
        CryptreeNode newFileAccess = fileAccess
                .withWriterLink(cap.rBaseKey, signerLink)
                .withParentLink(getParentKey(), newParentLink);
        WritableAbsoluteCapability ourNewCap = cap.withSigner(signer.publicKeyHash);
        RetrievedCapability newRetrievedCapability = new RetrievedCapability(ourNewCap, newFileAccess);

        // create the new signing subspace move subtree to it
        PublicKeyHash owner = owner();

        network.synchronizer.putEmpty(owner, signer.publicKeyHash);
        return network.synchronizer.applyComplexUpdate(owner, signer, (version, committer) -> IpfsTransaction.call(owner,
                tid -> network.uploadChunk(version, committer, newFileAccess, owner, getPointer().capability.getMapKey(), signer, tid)
                        .thenCompose(newVersion -> copyAllChunks(false, cap, signer, hasher, network, newVersion, committer))
                        .thenCompose(copiedVersion -> copiedVersion.withWriter(owner, parent.writer(), network))
                        .thenCompose(withParent -> parent.getPointer().fileAccess
                                .updateChildLink(withParent, committer, parent.writableFilePointer(),
                                        parent.signingPair(),
                                        getPointer(),
                                        newRetrievedCapability, network, random, hasher))
                        .thenCompose(updatedParentVersion -> deleteAllChunks(cap, signingPair(), tid, hasher, network,
                                updatedParentVersion, committer)),
                network.dhtClient)
        ).thenCompose(finalVersion -> parent.getUpdated(finalVersion, network)
                .thenCompose(updatedParent -> network.getFile(finalVersion, ourNewCap, Optional.of(signer), ownername)
                .thenApply(updatedUs -> new Pair<>(updatedUs.get(), updatedParent))));
    }

    /** This copies all the cryptree nodes from one signing key to another for a file or subtree
     *
     * @param includeFirst
     * @param currentCap
     * @param targetSigner
     * @param network
     * @return
     */
    private static CompletableFuture<Snapshot> copyAllChunks(boolean includeFirst,
                                                             AbsoluteCapability currentCap,
                                                             SigningPrivateKeyAndPublicHash targetSigner,
                                                             Hasher hasher,
                                                             NetworkAccess network,
                                                             Snapshot initialVersion,
                                                             Committer committer) {

        return initialVersion.withWriter(currentCap.owner, currentCap.writer, network)
                .thenCompose(version -> network.getMetadata(version.get(currentCap.writer).props, currentCap)
                        .thenCompose(mOpt -> {
                            if (! mOpt.isPresent()) {
                                return CompletableFuture.completedFuture(version);
                            }
                            return (includeFirst ?
                                    IpfsTransaction.call(currentCap.owner,
                                            tid -> network.addPreexistingChunk(mOpt.get(), currentCap.owner, currentCap.getMapKey(),
                                                    targetSigner, tid, version, committer), network.dhtClient) :
                                    CompletableFuture.completedFuture(version))
                                    .thenCompose(updated -> {
                                        CryptreeNode chunk = mOpt.get();
                                        Optional<byte[]> streamSecret = chunk.getProperties(chunk
                                                .getParentKey(currentCap.rBaseKey)).streamSecret;
                                        return chunk.getNextChunkLocation(currentCap.rBaseKey,
                                                streamSecret, currentCap.getMapKey(), currentCap.bat, hasher)
                                                .thenCompose(nextChunkMapKeyAndBat ->
                                                        copyAllChunks(true, currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right),
                                                                targetSigner, hasher, network, updated, committer));
                                    })
                                    .thenCompose(updatedVersion -> {
                                        if (! mOpt.get().isDirectory())
                                            return CompletableFuture.completedFuture(updatedVersion);
                                        return mOpt.get().getDirectChildrenCapabilities(currentCap, updatedVersion, network)
                                                .thenCompose(childCaps ->
                                                        Futures.reduceAll(childCaps,
                                                                updatedVersion,
                                                                (v, cap) -> copyAllChunks(true, cap.cap,
                                                                        targetSigner, hasher, network, v, committer),
                                                                (x, y) -> y));
                                    });
                        }));
    }

    public static CompletableFuture<Snapshot> deleteAllChunks(WritableAbsoluteCapability currentCap,
                                                              SigningPrivateKeyAndPublicHash signer,
                                                              TransactionId tid,
                                                              Hasher hasher,
                                                              NetworkAccess network,
                                                              Snapshot version,
                                                              Committer committer) {
        return version.withWriter(currentCap.owner, currentCap.writer, network)
                .thenCompose(current -> network.getMetadata(current.get(currentCap.writer).props, currentCap)
                        .thenCompose(mOpt -> {
                            if (! mOpt.isPresent()) {
                                return CompletableFuture.completedFuture(current);
                            }
                            CryptreeNode chunk = mOpt.get();
                            SigningPrivateKeyAndPublicHash ourSigner = chunk
                                    .getSigner(currentCap.rBaseKey, currentCap.wBaseKey.get(), Optional.of(signer));
                            FileProperties props = chunk.getProperties(chunk
                                    .getParentKey(currentCap.rBaseKey));
                            Optional<byte[]> streamSecret = props.streamSecret;

                            boolean normalFile = ! chunk.isDirectory() && streamSecret.isPresent();
                            return (normalFile ?
                                    deleteFileChunks(props.streamSecret.get(), props.chunkCount(), currentCap, ourSigner, tid, hasher, network, current, committer) :
                                    network.deleteChunk(current, committer, chunk, currentCap.owner,
                                                    currentCap.getMapKey(), ourSigner, tid)
                                            .thenCompose(deletedVersion -> {
                                                return chunk.getNextChunkLocation(currentCap.rBaseKey, streamSecret,
                                                        currentCap.getMapKey(), currentCap.bat, hasher).thenCompose(nextChunkMapKeyAndBat ->
                                                        deleteAllChunks(currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right), signer, tid, hasher,
                                                                network, deletedVersion, committer));
                                            }))
                                    .thenCompose(updatedVersion -> {
                                        if (! chunk.isDirectory())
                                            return CompletableFuture.completedFuture(updatedVersion);
                                        return chunk.getDirectChildrenCapabilities(currentCap, updatedVersion, network).thenCompose(childCaps ->
                                                Futures.reduceAll(childCaps,
                                                        updatedVersion,
                                                        (v, cap) -> deleteAllChunks((WritableAbsoluteCapability) cap.cap, signer,
                                                                tid, hasher, network, v, committer),
                                                        (x, y) -> y));
                                    })
                                    .thenCompose(s -> removeSigningKey(currentCap.writer, signer, currentCap.owner, network, s, committer));
                        }));
    }

    private static CompletableFuture<Snapshot> deleteFileChunks(byte[] streamSecret,
                                                                int nChunks,
                                                                WritableAbsoluteCapability startCap,
                                                                SigningPrivateKeyAndPublicHash ourSigner,
                                                                TransactionId tid,
                                                                Hasher hasher,
                                                                NetworkAccess network,
                                                                Snapshot current,
                                                                Committer c) {
        return getAllChunkLocations(startCap.getMapKey(), streamSecret, nChunks, hasher)
                .thenCompose(labels -> network.deleteAllChunksIfPresent(current, c, startCap.owner, ourSigner, labels, tid));
    }

    private static CompletableFuture<List<byte[]>> getAllChunkLocations(byte[] first, byte[] streamSecret, int nChunks, Hasher h) {
        List<byte[]> res = new ArrayList<>(nChunks);
        res.add(first);
        return Futures.reduceAll(IntStream.range(1, nChunks).mapToObj(i -> i), res,
                (labels, i) -> FileProperties.calculateNextMapKey(streamSecret, labels.get(labels.size() - 1), Optional.empty(), h)
                        .thenApply(next -> {
                            labels.add(next.left);
                            return labels;
                        }),
                (a, b) -> b);
    }

    /**
     * @param parent
     * @param userContext
     * @return updated parent
     */
    @JsMethod
    public CompletableFuture<FileWrapper> remove(FileWrapper parent, Path ourPath, UserContext userContext) {
        NetworkAccess network = userContext.network;
        Hasher hasher = userContext.crypto.hasher;
        ensureUnmodified();
        if (! pointer.capability.isWritable())
            return Futures.errored(new IllegalStateException("Cannot delete file without write access to it"));

        boolean writableParent = parent.isWritable();
        parent.setModified();
        network.disableCommits();
        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),
                (version, c) -> {
                    return (writableParent ? version.withWriter(owner(), parent.writer(), network)
                            .thenCompose(v2 -> parent.pointer.fileAccess
                                    .removeChildren(v2, c, Arrays.asList(getPointer().capability), parent.writableFilePointer(),
                                            parent.entryWriter, network, userContext.crypto.random, hasher)) :
                            Futures.of(version))
                            .thenCompose(v -> IpfsTransaction.call(owner(),
                                    tid -> FileWrapper.deleteAllChunks(
                                            isLink() ?
                                                    (WritableAbsoluteCapability) getLinkPointer().capability :
                                                    writableFilePointer(),
                                            writableParent ?
                                                    parent.signingPair() :
                                                    signingPair(), tid, hasher, network, v, c), network.dhtClient)
                                    .thenCompose(s -> userContext.isSecretLink() ? Futures.of(s) :
                    userContext.sharedWithCache.clearSharedWith(ourPath, s, c, network)));
                })
                .thenCompose(s -> parent.getUpdated(s, network));
    }

    public static CompletableFuture<Snapshot> removeSigningKey(PublicKeyHash signerToRemove,
                                                               SigningPrivateKeyAndPublicHash parentSigner,
                                                               PublicKeyHash owner,
                                                               NetworkAccess network,
                                                               Snapshot current,
                                                               Committer committer) {
        PublicKeyHash parentWriter = parentSigner.publicKeyHash;
        if (parentWriter.equals(signerToRemove))
            return CompletableFuture.completedFuture(current);

        return current.withWriter(owner, parentWriter, network)
                .thenCompose(s -> s.get(parentSigner).props
                        .removeOwnedKey(owner, parentSigner, signerToRemove, network.dhtClient, network.hasher)
                        .thenCompose(removed -> IpfsTransaction.call(
                                owner,
                                tid -> committer.commit(owner, parentSigner, removed, s.get(parentSigner), tid),
                                network.dhtClient))
                        .thenApply(committed -> s.withVersion(parentWriter, committed.get(parentWriter))));
    }

    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,
                                                                   Crypto crypto,
                                                                   ProgressConsumer<Long> monitor) {
        return network.synchronizer.getValue(owner(), writer())
                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, getFileProperties().size, monitor));
    }

    public CompletableFuture<? extends AsyncReader> getInputStream(WriterData version,
                                                                   NetworkAccess network,
                                                                   Crypto crypto,
                                                                   ProgressConsumer<Long> monitor) {
        return getInputStream(version, network, crypto, getFileProperties().size, monitor);
    }

    @JsMethod
    public CompletableFuture<? extends AsyncReader> getBufferedInputStream(NetworkAccess network,
                                                                           Crypto crypto,
                                                                           int fileSizeHi,
                                                                           int fileSizeLow,
                                                                           int bufferChunks,
                                                                           ProgressConsumer<Long> monitor) {
        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);
        return getInputStream(network, crypto, fileSizeHi, fileSizeLow, monitor)
                .thenApply(r -> new BufferedAsyncReader(r, bufferChunks, fileSize));
    }

    @JsMethod
    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,
                                                                   Crypto crypto,
                                                                   int fileSizeHi,
                                                                   int fileSizeLow,
                                                                   ProgressConsumer<Long> monitor) {
        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);
        return network.synchronizer.getValue(owner(), writer())
                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, fileSize, monitor));
    }

    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,
                                                                   Crypto crypto,
                                                                   long fileSize,
                                                                   ProgressConsumer<Long> monitor) {
        return network.synchronizer.getValue(owner(), writer())
                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, fileSize, monitor));
    }

    public CompletableFuture<? extends AsyncReader> getInputStream(WriterData version,
                                                                   NetworkAccess network,
                                                                   Crypto crypto,
                                                                   long fileSize,
                                                                   ProgressConsumer<Long> monitor) {
        ensureUnmodified();
        if (pointer.fileAccess.isDirectory())
            throw new IllegalStateException("Cannot get input stream for a directory!");
        CryptreeNode fileAccess = pointer.fileAccess;
        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)
                .thenCompose(retriever ->
                        retriever.getFile(version, network, crypto, pointer.capability, props.streamSecret,
                                fileSize, fileAccess.committedHash(), monitor));
    }

    private CompletableFuture<FileRetriever> getRetriever(Hasher hasher) {
        if (pointer.fileAccess.isDirectory())
            throw new IllegalStateException("Cannot get input stream for a directory!");
        return pointer.fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, hasher);
    }

    @JsMethod
    public String getBase64Thumbnail() {
        Optional<Thumbnail> thumbnail = props.thumbnail;
        if (thumbnail.isPresent()) {
            Thumbnail thumb = thumbnail.get();
            String base64Data = Base64.getEncoder().encodeToString(thumb.data);
            if (thumb.mimeType.equals("image/webp"))
                return "data:image/webp;base64," + base64Data;
            if (thumb.mimeType.equals("image/jpeg"))
                return "data:image/jpeg;base64," + base64Data;
            if (thumb.mimeType.equals("image/png"))
                return "data:image/png;base64," + base64Data;
            throw new IllegalStateException("Unknown thumbnail mimetype: " + thumb.mimeType);
        } else {
            return "";
        }
    }

    @JsMethod
    public FileProperties getFileProperties() {
        ensureUnmodified();
        return props;
    }

    @JsMethod
    public String getName() {
        return getFileProperties().name;
    }

    public long getSize() {
        return getFileProperties().size;
    }

    public String toString() {
        return getFileProperties().name;
    }

    public static FileWrapper createRoot(TrieNode root) {
        return new FileWrapper(Optional.of(root), null, Optional.empty(), Optional.empty(), null, new Snapshot(new HashMap<>()));
    }

    public static Optional<Thumbnail> generateVideoThumbnail(byte[] videoBlob) {
        File tempFile = null;
        try {
            tempFile = File.createTempFile(UUID.randomUUID().toString(), ".mp4");
            Files.write(tempFile.toPath(), videoBlob, StandardOpenOption.WRITE);
            return VideoThumbnail.create(tempFile.getAbsolutePath(), THUMBNAIL_SIZE, THUMBNAIL_SIZE);
        } catch (IOException ioe) {
            LOG.log(Level.WARNING, ioe.getMessage(), ioe);
        } finally {
            if(tempFile != null) {
                try {
                    Files.delete(tempFile.toPath());
                }catch(IOException ioe){

                }
            }
        }
        return Optional.empty();
    }

    private static Optional<Thumbnail> convertFromBase64(String base64Url) {
        String base64data = base64Url.substring(base64Url.indexOf(",") + 1);
        byte[] data = Base64.getDecoder().decode(base64data);
        if (data.length == 0)
            return Optional.empty();
        if (base64Url.startsWith("data:image/jpeg;base64,"))
            return Optional.of(new Thumbnail("image/jpeg", data));
        if (base64Url.startsWith("data:image/webp;base64,"))
            return Optional.of(new Thumbnail("image/webp", data));
        throw new IllegalStateException("Unknown image type for generated thumbnail!");
    }

    private CompletableFuture<Optional<Thumbnail>> generateThumbnail(NetworkAccess network, AsyncReader fileData, int fileSize, String filename, String mimeType) {
        CompletableFuture<Optional<Thumbnail>> fut = new CompletableFuture<>();
        if (fileSize > MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE) {
            if (mimeType.startsWith("image")) {
                if (network.isJavascript()) {
                    thumbnail.generateThumbnail(fileData, fileSize, filename).thenAccept(base64Str -> {
                        fut.complete(convertFromBase64(base64Str));
                    });
                } else {
                    byte[] bytes = new byte[fileSize];
                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {
                        fut.complete(ThumbnailGenerator.get().generateThumbnail(bytes));
                    }).exceptionally(t -> {
                        fut.complete(Optional.empty());
                        return null;
                    });
                }
            } else if (mimeType.startsWith("video")) {
                if (network.isJavascript()) {
                    thumbnail.generateVideoThumbnail(fileData, fileSize, filename, mimeType).thenAccept(base64Str -> {
                        if(base64Str == null) {
                            fut.complete(Optional.empty());
                        }
                        fut.complete(convertFromBase64(base64Str));
                    });
                } else {
                    // TODO find a cross platform way to generate (streaming) video thumbnails in Java
                    fut.complete(Optional.empty());
//                    byte[] bytes = new byte[fileSize];
//                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {
//                        fut.complete(generateVideoThumbnail(bytes));
//                    }).exceptionally(t -> {
//                        fut.complete(Optional.empty());
//                        return null;
//                    });
                }
            } else if (mimeType.startsWith("audio/mpeg")) {
                byte[] mp3Data = new byte[fileSize];
                fileData.readIntoArray(mp3Data, 0, fileSize).thenAccept(read -> {
                    try {
                        Mp3CoverImage mp3CoverImage = Mp3CoverImage.extractCoverArt(mp3Data);
                        if (mp3CoverImage.imageData == null) {
                            fut.complete(Optional.empty());
                        } else {
                            if (network.isJavascript()) {
                                AsyncReader.ArrayBacked imageBlob = new AsyncReader.ArrayBacked(mp3CoverImage.imageData);
                                thumbnail.generateThumbnail(imageBlob, mp3CoverImage.imageData.length, filename)
                                        .thenAccept(base64Str -> {
                                            fut.complete(convertFromBase64(base64Str));
                                        });
                            } else {
                                fut.complete(ThumbnailGenerator.get().generateThumbnail(mp3CoverImage.imageData));
                            }
                        }
                    } catch(Exception ex) {
                        fut.complete(Optional.empty());
                    }
                });
            } else {
                fut.complete(Optional.empty());
            }
        } else {
            fut.complete(Optional.empty());
        }
        return fut;
    }

    private static CompletableFuture<String> getFileType(AsyncReader imageBlob, String filename) {
        CompletableFuture<String> result = new CompletableFuture<>();
        byte[] data = new byte[MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE];
        imageBlob.readIntoArray(data, 0, data.length).thenAccept(numBytesRead -> {
            imageBlob.reset().thenAccept(resetResult -> {
                if (numBytesRead < data.length) {
                    result.complete("");
                } else {
                    String mimeType = MimeTypes.calculateMimeType(data, filename);
                    result.complete(mimeType);
                }
            });
        });
        return result;
    }

    public static CompletableFuture<String> calculateMimeType(AsyncReader data, long fileSize, String filename) {
        byte[] header = new byte[(int) Math.min(fileSize, MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE)];
        return data.readIntoArray(header, 0, header.length)
                .thenApply(read -> MimeTypes.calculateMimeType(header, filename));
    }
}

<MSG> Fix FileWrapper.getDescendentByPath on Windows

<DFF> @@ -206,8 +206,12 @@ public class FileWrapper {
 
         Path canon = PathUtil.get(path);
         return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {
-            if (child.isPresent())
-                return child.get().getDescendentByPath(canon.subpath(1, canon.getNameCount()).toString(), child.get().version, hasher, network);
+            if (child.isPresent()) {
+                int names = canon.getNameCount();
+                if (names == 1)
+                    return Futures.of(child);
+                return child.get().getDescendentByPath(canon.subpath(1, names).toString(), child.get().version, hasher, network);
+            }
             return CompletableFuture.completedFuture(Optional.empty());
         });
     }

{'commit': 'diff --git a/src/peergos/shared/user/fs/FileWrapper.java b/src/peergos/shared/user/fs/FileWrapper.java\nindex 96249442..4ef74209 100755\n--- a/src/peergos/shared/user/fs/FileWrapper.java\n+++ b/src/peergos/shared/user/fs/FileWrapper.java\n@@ -206,8 +206,12 @@ public class FileWrapper {\n \n         Path canon = PathUtil.get(path);\n         return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {\n-            if (child.isPresent())\n-                return child.get().getDescendentByPath(canon.subpath(1, canon.getNameCount()).toString(), child.get().version, hasher, network);\n+            if (child.isPresent()) {\n+                int names = canon.getNameCount();\n+                if (names == 1)\n+                    return Futures.of(child);\n+                return child.get().getDescendentByPath(canon.subpath(1, names).toString(), child.get().version, hasher, network);\n+            }\n             return CompletableFuture.completedFuture(Optional.empty());\n         });\n     }\n', 'commit_subject': 'Fix FileWrapper.getDescendentByPath on Windows', 'repo_name': 'Peergos/Peergos', 'committer': {'name': 'ian', 'email': 'bd7844bd6a333db3779f8a7d590cb44a8b402d28@protonmail.com', 'time_sec': '1647000977', 'tz_offset': '0', 'date': {'seconds': '1647000977'}}, 'commit_tree': '758c31aa1a6d69c0b5284d2ace3ec7055da79281', 'commit_message': 'Fix FileWrapper.getDescendentByPath on Windows\n', 'language_name': 'Java', 'license': 'agpl-3.0', 'hunks': '@@ -206,8 +206,12 @@ public class FileWrapper {\n \n         Path canon = PathUtil.get(path);\n         return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {\n-            if (child.isPresent())\n-                return child.get().getDescendentByPath(canon.subpath(1, canon.getNameCount()).toString(), child.get().version, hasher, network);\n+            if (child.isPresent()) {\n+                int names = canon.getNameCount();\n+                if (names == 1)\n+                    return Futures.of(child);\n+                return child.get().getDescendentByPath(canon.subpath(1, names).toString(), child.get().version, hasher, network);\n+            }\n             return CompletableFuture.completedFuture(Optional.empty());\n         });\n     }\n', 'addition_count': 6, 'deletion_count': 2, 'src_file': 'a/src/peergos/shared/user/fs/FileWrapper.java', 'tgt_file': 'b/src/peergos/shared/user/fs/FileWrapper.java', 'file_extension': '.java', 'before_file': 'package peergos.shared.user.fs;\nimport java.nio.file.*;\nimport java.util.logging.*;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.multihash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport javax.imageio.ImageIO;\nimport java.awt.image.BufferedImage;\nimport java.awt.Graphics2D;\nimport java.awt.AlphaComposite;\nimport java.awt.RenderingHints;\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This class is used to read and modify files and directories and represents a single file or directory and the keys\n *  to access it.\n *\n */\npublic class FileWrapper {\n    public static final long THUMBNAIL_PROGRESS_OFFSET = 20*1024;\n\tprivate static final Logger LOG = Logger.getGlobal();\n\n    private final static int THUMBNAIL_SIZE = 400;\n    private static final NativeJSThumbnail thumbnail = new NativeJSThumbnail();\n\n    private final RetrievedCapability pointer;\n    private final Optional<RetrievedCapability> linkPointer;\n    private final Optional<SigningPrivateKeyAndPublicHash> entryWriter;\n    private final FileProperties props;\n    private final String ownername;\n    private final Optional<TrieNode> capTrie;\n    public final Snapshot version;\n    private final boolean isWritable;\n    private AtomicBoolean modified = new AtomicBoolean(); // This only used as a guard against concurrent modifications\n\n    /**\n     *\n     * @param capTrie This is only present if this is the global root, or this is read only\n     * @param pointer\n     * @param ownername\n     */\n    public FileWrapper(Optional<TrieNode> capTrie,\n                       RetrievedCapability pointer,\n                       Optional<RetrievedCapability> linkPointer,\n                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                       String ownername,\n                       Snapshot version) {\n        this.capTrie = capTrie;\n        this.pointer = pointer;\n        this.linkPointer = linkPointer;\n        this.entryWriter = entryWriter;\n        this.ownername = ownername;\n        this.version = version;\n        this.isWritable = pointer != null &&\n                pointer.capability instanceof WritableAbsoluteCapability ||\n                entryWriter.map(s -> s.publicKeyHash.equals(pointer.capability.writer)).orElse(false);\n        if (pointer == null)\n            props = new FileProperties("/", true, false, "", 0, LocalDateTime.MIN, LocalDateTime.MIN, false, Optional.empty(), Optional.empty());\n        else {\n            SymmetricKey parentKey = this.getParentKey();\n            FileProperties directProps = pointer.fileAccess.getProperties(parentKey);\n            if (linkPointer.isPresent()) {\n                RetrievedCapability link = linkPointer.get();\n                FileProperties linkProps = link.getProperties();\n                this.props = directProps.withLink(linkProps);\n            } else {\n                this.props = directProps;\n            }\n        }\n        if (pointer != null && ! version.contains(pointer.capability.writer))\n            throw new IllegalStateException("File version doesn\'t include its own writer!");\n        if (isWritable() && !signingPair().publicKeyHash.equals(pointer.capability.writer))\n            throw new IllegalStateException("Invalid FileWrapper! public writing keys don\'t match!");\n    }\n\n    public FileWrapper(RetrievedCapability pointer,\n                       Optional<RetrievedCapability> linkPointer,\n                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                       String ownername,\n                       Snapshot version) {\n        this(Optional.empty(), pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withTrieNode(TrieNode trie) {\n        return new FileWrapper(Optional.of(trie), pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withTrieNodeOpt(Optional<TrieNode> trie) {\n        return new FileWrapper(trie, pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withLinkPointer(Optional<RetrievedCapability> link) {\n        return new FileWrapper(capTrie, pointer, link, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withVersion(Snapshot version) {\n        return new FileWrapper(capTrie, pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public CommittedWriterData getVersionRoot() {\n        return version.get(writer());\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> getLatest(NetworkAccess network) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(latest -> getUpdated(latest, network));\n\n    }\n\n    @JsMethod\n    public boolean samePointer(FileWrapper other) {\n        return pointer.equals(other.getPointer());\n    }\n\n    public CompletableFuture<FileWrapper> getUpdated(NetworkAccess network) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    public CompletableFuture<FileWrapper> getUpdated(Snapshot version, NetworkAccess network) {\n        return version.withWriter(owner(), writer(), network).thenCompose(v -> {\n            if (this.version.get(writer()).equals(v.get(writer())))\n                return CompletableFuture.completedFuture(this);\n            return network.getFile(v, pointer.capability, entryWriter, ownername)\n                    .thenApply(Optional::get)\n                    .thenApply(f -> f.withTrieNodeOpt(capTrie));\n        });\n    }\n\n    public PublicKeyHash owner() {\n        try {\n            return pointer.capability.owner;\n        } catch (Exception e)  {\n            System.out.println();\n            throw e;\n        }\n    }\n\n    public PublicKeyHash writer() {\n        return pointer.capability.writer;\n    }\n\n    public RetrievedCapability getPointer() {\n        return pointer;\n    }\n\n    public RetrievedCapability getLinkPointer() {\n        return linkPointer.get();\n    }\n\n    public boolean isRoot() {\n        return props.name.equals("/");\n    }\n\n    public CompletableFuture<String> getPath(NetworkAccess network) {\n        return retrieveParent(network).thenCompose(parent -> {\n            if (!parent.isPresent() || parent.get().isRoot())\n                return CompletableFuture.completedFuture("/" + props.name);\n            return parent.get().getPath(network).thenApply(parentPath -> parentPath + "/" + props.name);\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Multihash> getContentHash(NetworkAccess network, Crypto crypto) {\n        return getInputStream(network, crypto, x -> {})\n                .thenCompose(reader -> crypto.hasher.hash(reader, getSize()));\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        return getDescendentByPath(path, version, hasher, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,\n                                                                        Snapshot version,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        ensureUnmodified();\n        if (path.length() == 0)\n            return CompletableFuture.completedFuture(Optional.of(this));\n\n        if (path.equals("/"))\n            if (isDirectory())\n                return CompletableFuture.completedFuture(Optional.of(this));\n            else\n                return CompletableFuture.completedFuture(Optional.empty());\n\n        Path canon = PathUtil.get(path);\n        return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {\n            if (child.isPresent())\n                return child.get().getDescendentByPath(canon.subpath(1, canon.getNameCount()).toString(), child.get().version, hasher, network);\n            return CompletableFuture.completedFuture(Optional.empty());\n        });\n    }\n\n    private void ensureUnmodified() {\n        if (modified.get())\n            throw new IllegalStateException("This file has already been modified, use the returned instance");\n    }\n\n    private void setModified() {\n        if (modified.get())\n            throw new IllegalStateException("This file has already been modified, use the returned instance");\n        modified.set(true);\n    }\n\n    public CompletableFuture<Snapshot> updateChildLinks(\n            Snapshot version,\n            Committer committer,\n            Collection<Pair<AbsoluteCapability, NamedAbsoluteCapability>> childCases,\n            NetworkAccess network,\n            SafeRandom random,\n            Hasher hasher) {\n        return pointer.fileAccess\n                .updateChildLinks(version, committer, (WritableAbsoluteCapability) pointer.capability, signingPair(),\n                        childCases, network, random, hasher);\n    }\n\n    public CompletableFuture<Boolean> hasChildWithName(Snapshot version, String name, Hasher hasher, NetworkAccess network) {\n        ensureUnmodified();\n        return getChild(version, name, network)\n                .thenApply(Optional::isPresent);\n    }\n\n    /**\n     *\n     * @param child\n     * @param network\n     * @return Updated version of this directory without the child\n     */\n    public CompletableFuture<FileWrapper> removeChild(FileWrapper child, NetworkAccess network, SafeRandom random, Hasher hasher) {\n        setModified();\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (cwd, committer) -> pointer.fileAccess\n                .removeChildren(cwd, committer, Arrays.asList(child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher))\n                .thenCompose(newRoot -> getUpdated(newRoot, network));\n    }\n\n    public CompletableFuture<Snapshot> removeChild(Snapshot version,\n                                                   Committer committer,\n                                                   FileWrapper child,\n                                                   NetworkAccess network,\n                                                   SafeRandom random,\n                                                   Hasher hasher) {\n        return pointer.fileAccess.removeChildren(version, committer,\n                Arrays.asList(child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher);\n    }\n\n    @JsMethod\n    public String toLink() {\n        return pointer.capability.readOnly().toLink();\n    }\n\n    @JsMethod\n    public String toWritableLink() {\n        if (! isWritable())\n            throw new IllegalStateException("You do not have write access to " + getName());\n        return pointer.capability.toLink();\n    }\n\n    @JsMethod\n    public boolean isWritable() {\n        return isWritable;\n    }\n\n    @JsMethod\n    public boolean isReadable() {\n        return pointer.fileAccess.isReadable(pointer.capability.rBaseKey);\n    }\n\n    public SymmetricKey getKey() {\n        return pointer.capability.rBaseKey;\n    }\n\n    public Location getLocation() {\n        return new Location(pointer.capability.owner, pointer.capability.writer, pointer.capability.getMapKey());\n    }\n\n    public CompletableFuture<Set<NamedAbsoluteCapability>> getChildrenCapabilities(Hasher hasher, NetworkAccess network) {\n        ensureUnmodified();\n        if (!this.isDirectory())\n            return CompletableFuture.completedFuture(Collections.emptySet());\n        return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> retrieveParent(NetworkAccess network) {\n        ensureUnmodified();\n        return retrieveParent(linkPointer.orElse(pointer), ownername, version, network);\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getAnyLinkPointer(NetworkAccess network) {\n        if (pointer == null)\n            return CompletableFuture.completedFuture(Optional.empty());\n        AbsoluteCapability cap = pointer.capability;\n        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);\n        return parent.thenApply(parentRFP -> {\n            if (parentRFP == null)\n                return Optional.empty();\n            FileProperties parentProps = parentRFP.getProperties();\n            if (! parentProps.isLink)\n                return Optional.empty();\n            return Optional.of(parentRFP);\n        });\n    }\n\n    public static CompletableFuture<Optional<FileWrapper>> retrieveParent(RetrievedCapability pointer,\n                                                                          String ownerName,\n                                                                          Snapshot version,\n                                                                          NetworkAccess network) {\n        if (pointer == null)\n            return CompletableFuture.completedFuture(Optional.empty());\n        AbsoluteCapability cap = pointer.capability;\n        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);\n        return parent.thenCompose(parentRFP -> {\n            if (parentRFP == null)\n                return Futures.of(Optional.empty());\n            FileProperties parentProps = parentRFP.getProperties();\n            if (! parentProps.isLink)\n                return version.withWriter(parentRFP.capability.owner, parentRFP.capability.writer, network)\n                        .thenApply(fullVersion -> Optional.of(new FileWrapper(parentRFP, Optional.empty(),\n                                Optional.empty(), ownerName, fullVersion)));\n            return retrieveParent(parentRFP, ownerName, version, network);\n        });\n    }\n\n    @JsMethod\n    public boolean isUserRoot() {\n        if (pointer == null)\n            return false;\n        return ! pointer.fileAccess.hasParentLink(pointer.capability.rBaseKey);\n    }\n\n    public SymmetricKey getParentKey() {\n        return pointer.getParentKey();\n    }\n\n    private Optional<SigningPrivateKeyAndPublicHash> getChildsEntryWriter() {\n        return pointer.capability.wBaseKey\n                .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<FileWrapper>> getChildren(Hasher hasher, NetworkAccess network) {\n        if (capTrie.isPresent())\n            return capTrie.get().getChildren("", hasher, network);\n        return getChildren(version, hasher, network);\n    }\n\n    public CompletableFuture<Set<FileWrapper>> getChildren(Snapshot version, Hasher hasher, NetworkAccess network) {\n        if (capTrie.isPresent())\n            return capTrie.get().getChildren("", hasher, version.merge(this.version), network);\n        if (isReadable()) {\n            Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                    .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n            return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network)\n                    .thenCompose(childCaps -> getFiles(owner(), childCaps, childsEntryWriter, ownername, network, version));\n        }\n        throw new IllegalStateException("Unreadable FileWrapper!");\n    }\n\n    private static CompletableFuture<Set<FileWrapper>> getFiles(PublicKeyHash owner,\n                                                                Set<NamedAbsoluteCapability> caps,\n                                                                Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                                String ownername,\n                                                                NetworkAccess network,\n                                                                Snapshot version) {\n        Set<PublicKeyHash> childWriters = caps.stream()\n                .map(c -> c.cap.writer)\n                .collect(Collectors.toSet());\n        return version.withWriters(owner, childWriters, network)\n                .thenCompose(fullVersion -> network.retrieveAllMetadata(caps.stream().map(n -> n.cap).collect(Collectors.toList()), fullVersion)\n                        .thenCompose(rcs -> Futures.combineAll(rcs.stream()\n                                .map(rc -> {\n                                    FileProperties props = rc.getProperties();\n                                    if (! props.isLink)\n                                        return Futures.of(new FileWrapper(rc, Optional.empty(), entryWriter, ownername, fullVersion));\n                                    return NetworkAccess.getFileFromLink(owner, rc, entryWriter, ownername, network, version);\n                                })\n                                .collect(Collectors.toSet()))));\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<FileWrapper>> getChild(String name, Hasher hasher, NetworkAccess network) {\n        return getChild(version, name, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getChild(Snapshot version,\n                                                             String name,\n                                                             NetworkAccess network) {\n        if (capTrie.isPresent())\n            return capTrie.get().getByPath("/" + name, version.merge(this.version), network.hasher, network);\n        return pointer.fileAccess.getChild(name, pointer.capability, version, network.hasher, network)\n                .thenCompose(rcOpt -> {\n                    if (rcOpt.isEmpty())\n                        return Futures.of(Optional.empty());\n                    RetrievedCapability rc = rcOpt.get();\n                    FileProperties props = rc.getProperties();\n                    Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                            .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n                    if (! props.isLink)\n                        return version.withWriter(owner(), rc.capability.writer, network)\n                                .thenApply(fullVersion -> Optional.of(new FileWrapper(rc, Optional.empty(),\n                                        childsEntryWriter, ownername, fullVersion)));\n                    return version.withWriter(owner(), rc.capability.writer, network)\n                            .thenCompose(fullVersion ->\n                                    NetworkAccess.getFileFromLink(owner(), rc, childsEntryWriter, ownername, network, fullVersion)\n                                            .thenApply(Optional::of));\n                });\n    }\n\n    @JsMethod\n    public String getOwnerName() {\n        return ownername;\n    }\n\n    @JsMethod\n    public boolean isDirectory() {\n        boolean isNull = pointer == null;\n        return isNull || pointer.fileAccess.isDirectory();\n    }\n\n    public boolean isLink() {\n        return linkPointer.isPresent();\n    }\n\n    public boolean isDirty() {\n        ensureUnmodified();\n        return pointer.fileAccess.isDirty(pointer.capability.rBaseKey);\n    }\n\n    /**\n     *\n     * @param current\n     * @param committer\n     * @param network\n     * @param crypto\n     * @return updated cleaned version\n     */\n    public CompletableFuture<Pair<FileWrapper, Snapshot>> clean(Snapshot current,\n                                                                Committer committer,\n                                                                NetworkAccess network,\n                                                                Crypto crypto) {\n        if (!isDirty())\n            return CompletableFuture.completedFuture(new Pair<>(this, current));\n        if (isDirectory()) {\n            throw new IllegalStateException("Directories are never dirty (they are cleaned immediately)!");\n        } else {\n            WritableAbsoluteCapability currentCap = writableFilePointer();\n            boolean isLink = isLink();\n            Optional<RelativeCapability> parentCap = pointer.fileAccess.getParentCapability(pointer.capability.rBaseKey);\n            Location parentOrLinkLocation = isLink ?\n                    linkPointer.get().capability.getLocation() :\n                    parentCap.get().getLocation(owner(), writer());\n            SymmetricKey parentOrLinkParentKey = isLink ?\n                    linkPointer.get().getParentKey() :\n                    parentCap.get().rBaseKey;\n            Optional<Bat> parentOrLinkBat = isLink ?\n                    linkPointer.get().capability.bat :\n                    parentCap.get().bat;\n            return pointer.fileAccess.cleanAndCommit(current, committer, currentCap, currentCap, props.streamSecret,\n                    Optional.empty(), signingPair(), SymmetricKey.random(),\n                    parentOrLinkLocation, parentOrLinkBat, parentOrLinkParentKey, network, crypto)\n                    .thenCompose(cwd -> {\n                        setModified();\n                        return getUpdated(cwd, network).thenApply(updated -> new Pair<>(updated, cwd));\n                    });\n        }\n    }\n\n    public CompletableFuture<Pair<byte[], Optional<Bat>>> getMapKey(long offset, NetworkAccess network, Crypto crypto) {\n        CryptreeNode fileAccess = pointer.fileAccess;\n        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)\n                .thenCompose(retriever -> retriever\n                        .getMapLabelAt(version.get(writer()).props, writableFilePointer(),\n                                getFileProperties().streamSecret, offset, crypto.hasher, network)\n                        .thenApply(Optional::get));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> truncate(long newSize, NetworkAccess network, Crypto crypto) {\n        if (getSize() <= newSize)\n            return Futures.of(this);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->\n                truncate(current, committer, newSize, network, crypto)\n        ).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public CompletableFuture<Snapshot> truncate(Snapshot initialVersion, Committer committer, long newSize, NetworkAccess network, Crypto crypto) {\n        if (isDirectory())\n            return Futures.errored(new IllegalStateException("You cannot truncate a directory!"));\n        FileProperties props = getFileProperties();\n        if (props.size <= newSize)\n            return CompletableFuture.completedFuture(initialVersion);\n\n        return initialVersion.withWriter(owner(), writer(), network)\n                .thenCompose(snapshot -> getMapKey(newSize, network, crypto).thenCompose(endMapKey ->\n                        getInputStream(snapshot.get(writer()).props, network, crypto, props.size, x -> {}).thenCompose(originalReader -> {\n                            long startOfLastChunk = newSize - (newSize % Chunk.MAX_SIZE);\n                            return originalReader.seek(startOfLastChunk).thenCompose(seekedOriginal -> {\n                                byte[] lastChunk = new byte[(int)(newSize % Chunk.MAX_SIZE)];\n                                return seekedOriginal.readIntoArray(lastChunk, 0, lastChunk.length).thenCompose(read -> {\n                                    if (newSize <= Chunk.MAX_SIZE)\n                                        return CompletableFuture.completedFuture(snapshot);\n                                    int currentChunk = (int) (newSize / Chunk.MAX_SIZE);\n                                    return IpfsTransaction.call(owner(), tid ->\n                                                    deleteFileChunks(props.streamSecret.get(), props.chunkCount() - currentChunk, writableFilePointer().withMapKey(endMapKey.left, endMapKey.right),\n                                                            signingPair(), tid, crypto.hasher, network, snapshot, committer),\n                                            network.dhtClient);\n                                }).thenCompose(deleted -> pointer.fileAccess.updateProperties(deleted, committer, writableFilePointer(),\n                                        entryWriter, props.withSize(startOfLastChunk), network).thenCompose(resized ->\n                                        getUpdated(resized, network).thenCompose(f -> f.clean(resized, committer, network, crypto)\n                                                .thenCompose(p -> p.left.overwriteSection(p.right, committer,\n                                                AsyncReader.build(lastChunk), startOfLastChunk, newSize, network, crypto, x -> {})))));\n                            });\n                        }))\n                );\n    }\n\n    public static int getNumberOfChunks(long size) {\n        if (size == 0)\n            return 1;\n        return (int)((size + Chunk.MAX_SIZE - 1)/Chunk.MAX_SIZE);\n    }\n\n    public List<Location> generateChildLocationsFromSize(long fileSize, SafeRandom random) {\n        return generateChildLocations(getNumberOfChunks(fileSize), random);\n    }\n\n    public List<Location> generateChildLocations(int numberOfChunks,\n                                                        SafeRandom random) {\n        return IntStream.range(0, numberOfChunks + 1) //have to have one extra location\n                .mapToObj(e -> new Location(owner(), writer(), random.randomBytes(32)))\n                .collect(Collectors.toList());\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> appendFileJS(String filename,\n                                                       AsyncReader fileData,\n                                                       int lengthHi,\n                                                       int lengthLow,\n                                                       NetworkAccess network,\n                                                       Crypto crypto,\n                                                       ProgressConsumer<Long> monitor) {\n        long fileSize = LongUtil.intsToLong(lengthHi, lengthLow);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> getChild(s, filename, network).thenCompose(childOpt -> {\n                if (childOpt.isEmpty()) {\n                    throw new IllegalStateException("File does not exists with name " + filename);\n                } else {\n                    FileProperties props = childOpt.get().getFileProperties();\n                    if (props.isHidden) {\n                        throw new IllegalStateException("File is hidden " + filename);\n                    }\n                    long startIndex = props.size;\n                    return uploadFileSection(s, committer, filename, fileData,\n                            false, startIndex, startIndex + fileSize, Optional.empty(), false, true, false,\n                            network, crypto, monitor, crypto.random.randomBytes(32), Optional.empty(),\n                            Optional.of(Bat.random(crypto.random)), mirrorBatId());\n                }\n        })).thenCompose(finished -> getUpdated(finished, network));\n    }\n        @JsMethod\n    public CompletableFuture<FileWrapper> uploadFileJS(String filename,\n                                                       AsyncReader fileData,\n                                                       int lengthHi,\n                                                       int lengthLow,\n                                                       boolean overwriteExisting,\n                                                       Optional<BatId> mirrorBat,\n                                                       NetworkAccess network,\n                                                       Crypto crypto,\n                                                       ProgressConsumer<Long> monitor,\n                                                       TransactionService transactions,\n                                                       Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile) {\n        FileUploadProperties fileProps = new FileUploadProperties(filename, fileData, lengthHi, lengthLow, false, overwriteExisting, monitor);\n        FolderUploadProperties currentFolder = new FolderUploadProperties(Collections.emptyList(), Collections.singletonList(fileProps));\n        return uploadSubtree(Stream.of(currentFolder), mirrorBat, network, crypto, transactions, resumeFile, () -> true);\n    }\n\n    private CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> resumeUpload(FileUploadTransaction txn,\n                                                                                              AsyncReader data,\n                                                                                              ProgressConsumer<Long> monitor,\n                                                                                              Snapshot s,\n                                                                                              Committer c,\n                                                                                              NetworkAccess network,\n                                                                                              Crypto crypto) {\n        RelativeCapability fromParent = writableFilePointer().relativise(txn.writeCap());\n        FileProperties props = txn.props;\n        // first find how many chunks were already uploaded, then seek reader to that offset and continue\n        return findFirstAbsentChunkIndex(0, txn.streamSecret(), txn.getFirstLocation(), s, network, crypto)\n                .thenCompose(startChunkIndex -> {\n                    monitor.accept(startChunkIndex * Chunk.MAX_SIZE);\n                    FileUploader uploader = new FileUploader(txn.targetFilename(), data, startChunkIndex*Chunk.MAX_SIZE,\n                            txn.size(), txn.baseKey, txn.dataKey, getLocation(), getPointer().capability.bat, getParentKey(),\n                            monitor, props, txn.getFirstLocation().getMapKey(), txn.firstBat);\n                    return uploader.uploadFrom(s, c, network, startChunkIndex.intValue(), txn.getFirstLocation().owner,\n                            signingPair(), mirrorBatId(), crypto.random, crypto.hasher);\n                }).thenApply(v -> new Pair<>(v, Optional.of(new NamedRelativeCapability(txn.targetFilename(), fromParent))));\n    }\n\n    private CompletableFuture<Long> findFirstAbsentChunkIndex(long currentIndex, byte[] streamSecret, Location currentLoc, Snapshot s, NetworkAccess network, Crypto crypto) {\n        return network.chunkIsPresent(s, currentLoc.owner, currentLoc.writer, currentLoc.getMapKey())\n                .thenCompose(present -> present ?\n                        FileProperties.calculateNextMapKey(streamSecret, currentLoc.getMapKey(), Optional.empty(), crypto.hasher)\n                                .thenCompose(p -> findFirstAbsentChunkIndex(currentIndex + 1, streamSecret, currentLoc.withMapKey(p.left), s, network, crypto)) :\n                        Futures.of(currentIndex));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              int fileSizeHi,\n                                                              int fileSizeLow,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              ProgressConsumer<Long> monitor) {\n        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);\n        return uploadOrReplaceFile(filename, fileData, fileSize, network, crypto, monitor,\n                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              ProgressConsumer<Long> monitor) {\n        return uploadOrReplaceFile(filename, fileData, length, network, crypto, monitor,\n                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              ProgressConsumer<Long> monitor,\n                                                              byte[] firstChunkMapKey,\n                                                              Optional<Bat> firstChunkBat,\n                                                              Optional<BatId> mirrorBat) {\n        return uploadFileSection(filename, fileData, false, 0, length, Optional.empty(),\n                true, network, crypto, monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat)\n                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)\n                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto))\n                        .thenCompose(c -> f.getUpdated(f.version.mergeAndOverwriteWith(c.version), network)));\n    }\n\n    public CompletableFuture<Snapshot> uploadOrReplaceFile(String filename,\n                                                           AsyncReader fileData,\n                                                           long length,\n                                                           boolean isHidden,\n                                                           Snapshot s,\n                                                           Committer c,\n                                                           NetworkAccess network,\n                                                           Crypto crypto,\n                                                           ProgressConsumer<Long> monitor,\n                                                           byte[] firstChunkMapKey,\n                                                           Optional<Bat> firstChunkBat,\n                                                           Optional<BatId> mirrorBat) {\n        return uploadFileSection(s, c, filename, fileData, isHidden, 0, length,\n                Optional.empty(), false, true, true, network, crypto,\n                monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat);\n    }\n\n    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              boolean isHidden,\n                                                              Optional<BatId> mirrorBat,\n                                                              NetworkAccess network,\n                                                              Crypto crypto) {\n        return uploadAndReturnFile(filename, fileData, length, isHidden, x -> {}, mirrorBat, network, crypto);\n    }\n\n    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              boolean isHidden,\n                                                              ProgressConsumer<Long> progressMonitor,\n                                                              Optional<BatId> mirrorBat,\n                                                              NetworkAccess network,\n                                                              Crypto crypto) {\n        return uploadFileSection(filename, fileData, isHidden, 0, length, Optional.empty(),\n                true, network, crypto, progressMonitor, crypto.random.randomBytes(32), Optional.empty(),\n                Optional.of(Bat.random(crypto.random)), mirrorBat)\n                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)\n                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto)));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> overwriteFileJS(AsyncReader fileData,\n                                                          int endHigh,\n                                                          int endLow,\n                                                          NetworkAccess network,\n                                                          Crypto crypto,\n                                                          ProgressConsumer<Long> monitor) {\n        long newSize = LongUtil.intsToLong(endHigh, endLow);\n        return overwriteFile(fileData, newSize, network, crypto, monitor);\n    }\n\n    public CompletableFuture<Snapshot> overwriteFile(AsyncReader fileData,\n                                                     long newSize,\n                                                     NetworkAccess network,\n                                                     Crypto crypto,\n                                                     ProgressConsumer<Long> monitor,\n                                                     Snapshot s,\n                                                     Committer committer) {\n        long size = getSize();\n        return clean(s, committer, network, crypto)\n                .thenCompose(u -> u.left.overwriteSection(u.right, committer, fileData,\n                        0L, newSize, network, crypto, monitor))\n                .thenCompose(v -> newSize >= size ?\n                        Futures.of(v) :\n                        getUpdated(v, network)\n                                .thenCompose(f -> f.truncate(v, committer, newSize, network, crypto)));\n    }\n\n    public CompletableFuture<FileWrapper> overwriteFile(AsyncReader fileData,\n                                                        long newSize,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> overwriteFile(fileData, newSize, network, crypto, monitor, s, committer))\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> overwriteSectionJS(AsyncReader fileData,\n                                                             int startHigh,\n                                                             int startLow,\n                                                             int endHigh,\n                                                             int endLow,\n                                                             NetworkAccess network,\n                                                             Crypto crypto,\n                                                             ProgressConsumer<Long> monitor) {\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> overwriteSection(s, committer, fileData,\n                        LongUtil.intsToLong(startHigh, startLow),\n                        LongUtil.intsToLong(endHigh, endLow), network, crypto, monitor))\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    public static final class FileUploadProperties {\n        public final String filename;\n        public final AsyncReader fileData;\n        public final long length;\n        public final boolean skipExisting;\n        public final boolean overwriteExisting;\n        public final ProgressConsumer<Long> monitor;\n\n        @JsConstructor\n        public FileUploadProperties(String filename,\n                                    AsyncReader fileData,\n                                    int lengthHi,\n                                    int lengthLow,\n                                    boolean skipExisting,\n                                    boolean overwriteExisting,\n                                    ProgressConsumer<Long> monitor) {\n            this.filename = filename;\n            this.fileData = fileData;\n            this.length = (((long)lengthHi) << 32) | (lengthLow & 0xFFFFFFFFL);\n            this.skipExisting = skipExisting;\n            this.overwriteExisting = overwriteExisting;\n            this.monitor = monitor;\n        }\n    }\n\n    public static class FolderUploadProperties {\n        public final List<String> relativePath;\n        public final List<FileUploadProperties> files;\n\n        @JsConstructor\n        public FolderUploadProperties(List<String> relativePath, List<FileUploadProperties> files) {\n            this.relativePath = relativePath;\n            this.files = files;\n        }\n\n        public Path path() {\n            return PathUtil.get(relativePath.stream().collect(Collectors.joining("/")));\n        }\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> uploadSubtree(Stream<FolderUploadProperties> directories,\n                                                        Optional<BatId> mirrorBat,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        TransactionService txns,\n                                                        Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,\n                                                        Supplier<Boolean> commitWatcher) {\n        // only use the supplied mirror BAT if the parent doesn\'t have a mirror BAT\n        Optional<BatId> mirror = mirrorBatId().or(() -> mirrorBat);\n        return getPath(network).thenCompose(path ->\n                network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                        (s, c) -> {\n                            return getUpdated(s, network).thenCompose(us -> Futures.reduceAll(directories, us,\n                                    (dir, children) -> dir.getOrMkdirs(children.relativePath, false, mirror, network, crypto, dir.version, c)\n                                            .thenCompose(p -> uploadFolder(PathUtil.get(path).resolve(children.path()), p.right,\n                                                    children, mirrorBat, txns, resumeFile, commitWatcher, network, crypto, c)\n                                                    .thenCompose(v -> dir.getUpdated(v, network))),\n                                    (a, b) -> b))\n                                    .thenApply(d -> d.version);\n                        },\n                        commitWatcher\n                )).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public static CompletableFuture<Snapshot> uploadFolder(Path toParent,\n                                                           FileWrapper parent,\n                                                           FolderUploadProperties children,\n                                                           Optional<BatId> mirrorBat,\n                                                           TransactionService transactions,\n                                                           Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,\n                                                           Supplier<Boolean> commitWatcher,\n                                                           NetworkAccess network,\n                                                           Crypto crypto,\n                                                           Committer c) {\n        Pair<Snapshot, List<NamedRelativeCapability>> identity = new Pair<>(parent.version, Collections.emptyList());\n\n        // Upload in order of ascending file size\n        List<FileUploadProperties> sortedChildren = children.files.stream()\n                .sorted(Comparator.comparingLong(a -> a.length))\n                .collect(Collectors.toList());\n        return Futures.reduceAll(sortedChildren, identity,\n                        (p, f) -> {\n                            // don\'t bother with file upload transactions as single chunk uploads are atomic anyway\n                            // (nothing to resume or cleanup later in case of failure)\n                            if (f.length <= Chunk.MAX_SIZE || transactions == null) // small files or writable public links\n                                return parent.uploadFileSection(p.left, c, f.filename, f.fileData, Optional.empty(), false, 0, f.length,\n                                                Optional.empty(), Optional.empty(), Optional.empty(), f.skipExisting,\n                                                f.overwriteExisting, true, network.disableCommits(), crypto, f.monitor,\n                                                crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), mirrorBat)\n                                        .thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())))\n                                        .thenCompose(r -> {\n                                            if (! network.isFull())\n                                                return Futures.of(r);\n                                            return atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto);\n                                        });\n\n                            network.enableCommits();\n                            List<FileUploadTransaction> toClose = new ArrayList<>();\n                            LocalDateTime now = LocalDateTime.now();\n                            return calculateMimeType(f.fileData, f.length, f.filename).thenCompose(mimeType -> {\n                                FileProperties props = new FileProperties(f.filename,\n                                        false, false, mimeType, f.length,\n                                        now, now, false, Optional.empty(), Optional.of(crypto.random.randomBytes(32)));\n                                return Transaction.buildFileUploadTransaction(toParent.resolve(f.filename).toString(), f.length,\n                                        props, props.streamSecret.get(), SymmetricKey.random(), SymmetricKey.random(),\n                                        SymmetricKey.random(), parent.signingPair(), new Location(parent.owner(), parent.writer(),\n                                                crypto.random.randomBytes(32)), Optional.of(Bat.random(crypto.random)), crypto.hasher);\n                            }).thenCompose(txn -> transactions.open(p.left, c, txn)\n                                            .thenCompose(r -> {\n                                                if (r.isB()) // we must clear legacy transactions which can\'t be resumed or ones whose parent has rotated writer\n                                                    return (r.b().isLegacy() || ! parent.writer().equals(r.b().writer()) ? Futures.of(false) : resumeFile.apply(r.b()))\n                                                            .thenCompose(resume -> {\n                                                                if (resume) {\n                                                                    toClose.add(r.b());\n                                                                    return parent.resumeUpload(r.b(), f.fileData, f.monitor, p.left, c, network, crypto)\n                                                                            .thenCompose(res -> f.fileData.reset().thenCompose(resetAgain ->\n                                                                                            parent.generateThumbnailAndUpdate(res.left, c, r.b().writeCap(), f.filename, resetAgain,\n                                                                                                    network, false, r.b().props.mimeType,\n                                                                                                    f.length, r.b().startTime(), r.b().startTime(), Optional.of(r.b().streamSecret()), f.monitor))\n                                                                                    .thenApply(s -> new Pair<>(s, res.right)));\n                                                                }\n                                                                return transactions.close(p.left, c, r.b())\n                                                                        .thenCompose(s2 -> transactions.open(s2, c, txn))\n                                                                        .thenCompose(r2 -> {\n                                                                            if (r2.isB())\n                                                                                throw new IllegalStateException("Error uploading file - concurrent upload of same file?");\n                                                                            toClose.add(txn);\n                                                                            return f.fileData.reset().thenCompose(reset -> parent.uploadFileSection(r2.a(), c, f.filename, reset, Optional.empty(),\n                                                                                    false, 0, f.length, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey),\n                                                                                    f.skipExisting, f.overwriteExisting, true,\n                                                                                    network, crypto, f.monitor, txn.firstMapKey(),\n                                                                                    Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat));\n                                                                        });\n                                                            }).thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())));\n                                                toClose.add(txn);\n                                                return f.fileData.reset().thenCompose(reset -> parent.uploadFileSection(r.a(), c, f.filename, f.fileData, Optional.empty(), false,\n                                                                0, f.length, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey),f.skipExisting, f.overwriteExisting, true,\n                                                                network, crypto, f.monitor, txn.firstMapKey(), Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat))\n                                                        .thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())));\n                                            })\n                            ).thenCompose(r -> atomicallyClearTransactionsAndAddToParent(toClose, r.right, parent, transactions, r.left, c, commitWatcher, network, crypto));\n                        },\n                        (a, b) -> new Pair<>(b.left, Stream.concat(a.right.stream(), b.right.stream()).collect(Collectors.toList())))\n                .thenCompose(r -> atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto))\n                .thenApply(x -> x.left);\n    }\n\n    private static CompletableFuture<Pair<Snapshot, List<NamedRelativeCapability>>> atomicallyClearTransactionsAndAddToParent(\n            List<FileUploadTransaction> toClose,\n            List<NamedRelativeCapability> childLinks,\n            FileWrapper parent,\n            TransactionService transactions,\n            Snapshot in,\n            Committer c,\n            Supplier<Boolean> commitWatcher,\n            NetworkAccess network,\n            Crypto crypto) {\n        if (toClose.isEmpty() && childLinks.isEmpty())\n            return Futures.of(new Pair<>(in, childLinks));\n        return parent.getUpdated(in, network)\n                .thenCompose(latest -> latest.addChildPointers(in, c, childLinks, network.disableCommits(), crypto))\n                .thenCompose(res -> Futures.reduceAll(toClose, res, (v, f) -> transactions.close(v, c, f), (a, b) -> b))\n                .thenApply(s -> {network.enableCommits(); return s;})\n                .thenApply(s -> new Pair<>(s, Collections.<NamedRelativeCapability>emptyList()));\n    }\n\n    public Optional<BatId> mirrorBatId() {\n        return pointer.fileAccess.mirrorBatId();\n    }\n\n    public CompletableFuture<Snapshot> overwriteSection(Snapshot current,\n                                                        Committer committer,\n                                                        AsyncReader fileData,\n                                                        long inputStartIndex,\n                                                        long endIndex,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        if (! isWritable())\n            return Futures.errored(new IllegalStateException("Unable to modify file without write access!"));\n        if (isDirty())\n            return Futures.errored(new IllegalStateException("File needs cleaning before modification."));\n\n        FileProperties props = getFileProperties();\n        String filename = props.name;\n        LOG.info("Overwriting section [" + Long.toHexString(inputStartIndex) + ", " + Long.toHexString(endIndex) + "] of: " + filename);\n\n        Supplier<Location> legacyLocs = () -> new Location(getLocation().owner, getLocation().writer, crypto.random.randomBytes(32));\n\n        SymmetricKey parentParentKey = getPointer().getParentParentKey();\n        Location parentLocation = getPointer().getParentCap().getLocation(owner(), writer());\n        Optional<Bat> parentBat = getPointer().getParentCap().bat;\n        WritableAbsoluteCapability ourCap = writableFilePointer();\n        return current.withWriter(owner(), writer(), network)\n                .thenCompose(base -> {\n                    FileWrapper us = this;\n                    final AtomicLong filesSize = new AtomicLong(props.size);\n                    return us.getRetriever(crypto.hasher).thenCompose(retriever -> {\n                        SymmetricKey baseKey = us.pointer.capability.rBaseKey;\n                        CryptreeNode fileAccess = us.pointer.fileAccess;\n                        SymmetricKey dataKey = fileAccess.getDataKey(baseKey);\n\n                        List<Long> startIndexes = new ArrayList<>();\n\n                        for (long startIndex = inputStartIndex; startIndex < endIndex; startIndex = startIndex + Chunk.MAX_SIZE - (startIndex % Chunk.MAX_SIZE))\n                            startIndexes.add(startIndex);\n\n                        BiFunction<Snapshot, Long, CompletableFuture<Snapshot>> composer = (version, startIndex) -> {\n                            MaybeMultihash currentHash = us.pointer.fileAccess.committedHash();\n                            return retriever.getChunk(version.get(us.writer()).props, network, crypto, startIndex,\n                                    filesSize.get(), ourCap, props.streamSecret, currentHash, monitor)\n                                    .thenCompose(currentLocation -> {\n                                                CompletableFuture<Optional<Pair<Location, Optional<Bat>>>> locationAt = retriever\n                                                        .getMapLabelAt(version.get(us.writer()).props, ourCap,\n                                                                props.streamSecret, startIndex + Chunk.MAX_SIZE, crypto.hasher, network)\n                                                        .thenApply(x -> x.map(mb -> new Pair<>(getLocation().withMapKey(mb.left), mb.right)));\n                                                return locationAt.thenCompose(locationAndBat ->\n                                                        CompletableFuture.completedFuture(new Pair<>(currentLocation, locationAndBat)));\n                                            }\n                                    ).thenCompose(pair -> {\n\n                                        if (!pair.left.isPresent()) {\n                                            CompletableFuture<Snapshot> result = new CompletableFuture<>();\n                                            result.completeExceptionally(new IllegalStateException("Current chunk not present"));\n                                            return result;\n                                        }\n\n                                        LocatedChunk currentOriginal = pair.left.get();\n                                        Optional<Pair<Location, Optional<Bat>>> nextChunkLocationOpt = pair.right;\n                                        CompletableFuture<Pair<Location, Optional<Bat>>> nextChunkLocationFut = nextChunkLocationOpt\n                                                .map(Futures::of)\n                                                .orElseGet(() -> props.streamSecret\n                                                        .map(streamSecret -> FileProperties.calculateNextMapKey(streamSecret,\n                                                                currentOriginal.location.getMapKey(), currentOriginal.bat, crypto.hasher)\n                                                                .thenApply(nextMapKeyAndBat -> new Pair<>(us.getLocation().withMapKey(nextMapKeyAndBat.left), nextMapKeyAndBat.right)))\n                                                        .orElseGet(() -> Futures.of(new Pair<>(legacyLocs.get(), Optional.empty()))));\n                                        return nextChunkLocationFut.thenCompose(nextChunkLocationAndBat -> {\n                                            Location nextChunkLocation = nextChunkLocationAndBat.left;\n                                            Optional<Bat> nextChunkBat = nextChunkLocationAndBat.right;\n                                            LOG.info("********** Writing to chunk at mapkey: " + ArrayOps.bytesToHex(currentOriginal.location.getMapKey()) + " next: " + nextChunkLocation);\n\n                                            // modify chunk, re-encrypt and upload\n                                            int internalStart = (int) (startIndex % Chunk.MAX_SIZE);\n                                            int internalEnd = endIndex - (startIndex - internalStart) > Chunk.MAX_SIZE ?\n                                                    Chunk.MAX_SIZE : (int) (endIndex - (startIndex - internalStart));\n                                            byte[] rawData = currentOriginal.chunk.data();\n                                            // extend data array if necessary\n                                            if (rawData.length < internalEnd)\n                                                rawData = Arrays.copyOfRange(rawData, 0, internalEnd);\n                                            byte[] raw = rawData;\n                                            Optional<SymmetricLinkToSigner> writerLink = startIndex < Chunk.MAX_SIZE ?\n                                                    us.pointer.fileAccess.getWriterLink(us.pointer.capability.rBaseKey) :\n                                                    Optional.empty();\n\n                                            return fileData.readIntoArray(raw, internalStart, internalEnd - internalStart).thenCompose(read -> {\n\n                                                Chunk updated = new Chunk(raw, dataKey, currentOriginal.location.getMapKey(), dataKey.createNonce());\n                                                LocatedChunk located = new LocatedChunk(currentOriginal.location, currentOriginal.bat, currentOriginal.existingHash, updated);\n                                                long currentSize = filesSize.get();\n                                                FileProperties newProps = new FileProperties(props.name, false,\n                                                        props.isLink, props.mimeType,\n                                                        endIndex > currentSize ? endIndex : currentSize,\n                                                        LocalDateTime.now(), props.created, props.isHidden,\n                                                        props.thumbnail, props.streamSecret);\n\n                                                CompletableFuture<Snapshot> chunkUploaded = FileUploader.uploadChunk(version, committer, us.signingPair(),\n                                                        newProps, parentLocation, parentBat, parentParentKey, baseKey, located,\n                                                        nextChunkLocation, nextChunkBat, writerLink, mirrorBatId(),\n                                                        crypto.random, crypto.hasher, network, monitor);\n\n                                                return chunkUploaded.thenCompose(updatedBase -> {\n                                                    //update indices to be relative to next chunk\n                                                    long updatedLength = startIndex + internalEnd - internalStart;\n                                                    if (updatedLength > filesSize.get()) {\n                                                        filesSize.set(updatedLength);\n\n                                                        if (updatedLength > Chunk.MAX_SIZE) {\n                                                            // update file size in FileProperties of first chunk\n                                                            return network.getFile(updatedBase, ourCap, entryWriter, ownername)\n                                                                    .thenCompose(updatedUs -> {\n                                                                        FileProperties correctedSize = updatedUs.get()\n                                                                                .getPointer().fileAccess.getProperties(ourCap.rBaseKey)\n                                                                                .withSize(endIndex);\n                                                                        return updatedUs.get()\n                                                                                .getPointer().fileAccess.updateProperties(updatedBase,\n                                                                                        committer, ourCap, entryWriter, correctedSize, network);\n                                                                    });\n                                                        }\n                                                    }\n                                                    return CompletableFuture.completedFuture(updatedBase);\n                                                });\n                                            });\n                                        });\n                                    });\n                        };\n\n                        return Futures.reduceAll(startIndexes, base, composer, (a, b) -> b);\n                    });\n                });\n    }\n\n    public CompletableFuture<FileWrapper> uploadFileSection(String filename,\n                                                            AsyncReader fileData,\n                                                            boolean isHidden,\n                                                            long startIndex,\n                                                            long endIndex,\n                                                            Optional<SymmetricKey> baseKey,\n                                                            boolean overwriteExisting,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            ProgressConsumer<Long> monitor) {\n        return uploadFileSection(filename, fileData, isHidden, startIndex, endIndex, baseKey, overwriteExisting,\n                network, crypto, monitor, crypto.random.randomBytes(32), Optional.empty(),\n                Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    /**\n     *\n     * @param filename\n     * @param fileData\n     * @param isHidden\n     * @param startIndex\n     * @param endIndex\n     * @param baseKey The desired base key for the uploaded file. If absent a random key is generated.\n     * @param overwriteExisting\n     * @param network\n     * @param crypto\n     * @param monitor A way to report back progress in number of bytes of file written\n     * @param firstChunkMapKey The planned location for the first chunk\n     * @return The updated version of this directory after the upload\n     */\n    public CompletableFuture<FileWrapper> uploadFileSection(String filename,\n                                                            AsyncReader fileData,\n                                                            boolean isHidden,\n                                                            long startIndex,\n                                                            long endIndex,\n                                                            Optional<SymmetricKey> baseKey,\n                                                            boolean overwriteExisting,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            ProgressConsumer<Long> monitor,\n                                                            byte[] firstChunkMapKey,\n                                                            Optional<byte[]> streamSecret,\n                                                            Optional<Bat> firstBat,\n                                                            Optional<BatId> mirrorBat) {\n        if (isWritable())\n            return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->\n                    uploadFileSection(current, committer, filename, fileData, isHidden, startIndex, endIndex,\n                            baseKey, false, overwriteExisting, false, network, crypto, monitor, firstChunkMapKey, streamSecret, firstBat, mirrorBat))\n                    .thenCompose(finalBase -> getUpdated(finalBase, network));\n\n        if (! overwriteExisting)\n            return Futures.errored(new IllegalStateException("Cannot upload a file to a directory without write access!"));\n        return getChild(filename, crypto.hasher, network)\n                .thenCompose(c -> {\n                    if (! c.isPresent())\n                        return Futures.errored(new IllegalStateException("No child with name " + filename));\n                    FileWrapper child = c.get();\n                    return network.synchronizer.applyComplexUpdate(owner(), child.signingPair(),\n                            (current, committer) -> updateExistingChild(current, committer, child,\n                                    fileData, startIndex, endIndex, network, crypto, monitor))\n                            .thenApply(childVersion -> withVersion(version.mergeAndOverwriteWith(childVersion)));\n                });\n    }\n\n    private CompletableFuture<Snapshot> updateSize(Committer committer,\n                                                   long newSize,\n                                                   NetworkAccess network) {\n        FileProperties newProps = getFileProperties().withSize(newSize);\n        return updateProperties(version, committer, newProps, network);\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> updateThumbnail(String base64Str, NetworkAccess network) {\n        Optional<Thumbnail> thumbData = Optional.empty();\n        if (base64Str != null && base64Str.length() > 0) {\n            thumbData = convertFromBase64(base64Str);\n        }\n        FileProperties updatedProperties = this.props.withThumbnail(thumbData);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> updateProperties(s, committer, updatedProperties, network)\n        ).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public CompletableFuture<Snapshot> uploadFileSection(Snapshot initialVersion,\n                                                         Committer committer,\n                                                         String filename,\n                                                         AsyncReader fileData,\n                                                         boolean isHidden,\n                                                         long startIndex,\n                                                         long endIndex,\n                                                         Optional<SymmetricKey> baseKey,\n                                                         boolean skipExisting,\n                                                         boolean overwriteExisting,\n                                                         boolean truncateExisting,\n                                                         NetworkAccess network,\n                                                         Crypto crypto,\n                                                         ProgressConsumer<Long> monitor,\n                                                         byte[] firstChunkMapKey,\n                                                         Optional<byte[]> streamSecret,\n                                                         Optional<Bat> firstBat,\n                                                         Optional<BatId> requestedMirrorBat) {\n        return uploadFileSection(initialVersion, committer, filename, fileData, Optional.empty(), isHidden, startIndex, endIndex,\n                baseKey, Optional.empty(), Optional.empty(), skipExisting, overwriteExisting, truncateExisting, network, crypto, monitor, firstChunkMapKey, streamSecret, firstBat, requestedMirrorBat)\n                .thenCompose(p -> getUpdated(p.left, network)\n                        .thenCompose(latest -> p.right.isEmpty() ?\n                                Futures.of(p.left) :\n                                latest.addChildPointer(p.left, committer, p.right.get(), network, crypto)));\n    }\n\n    private CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> uploadFileSection(\n            Snapshot initialVersion,\n            Committer committer,\n            String filename,\n            AsyncReader fileData,\n            Optional<Thumbnail> existingThumbnail,\n            boolean isHidden,\n            long startIndex,\n            long endIndex,\n            Optional<SymmetricKey> requestedBaseKey,\n            Optional<SymmetricKey> requestedDataKey,\n            Optional<SymmetricKey> requestedWriteKey,\n            boolean skipExisting,\n            boolean overwriteExisting,\n            boolean truncateExisting,\n            NetworkAccess network,\n            Crypto crypto,\n            ProgressConsumer<Long> monitor,\n            byte[] firstChunkMapKey,\n            Optional<byte[]> streamSecret,\n            Optional<Bat> firstBat,\n            Optional<BatId> requestedMirrorBat) {\n        if (!isLegalName(filename)) {\n            return Futures.errored(new IllegalStateException("Illegal filename: " + filename));\n        }\n        if (! isDirectory()) {\n            return Futures.errored(new IllegalStateException("Cannot upload a sub file to a file!"));\n        }\n        Optional<BatId> mirrorBat = mirrorBatId().or(() -> requestedMirrorBat);\n        return initialVersion.withWriter(owner(), writer(), network)\n                .thenCompose(current -> getUpdated(current, network)\n                        .thenCompose(latest -> latest.getChild(current, filename, network)\n                                        .thenCompose(childOpt -> {\n                                            if (childOpt.isPresent()) {\n                                                if (skipExisting)\n                                                    return Futures.of(new Pair<>(current, Optional.empty()));\n                                                if (! overwriteExisting)\n                                                    throw new FileExistsException(filename);\n                                                FileWrapper child = childOpt.get();\n                                                FileProperties childProps = child.getFileProperties();\n\n                                                TriFunction<FileWrapper, Snapshot, Long, CompletableFuture<Snapshot>> updatePropsIfNecessary =\n                                                        (updatedChild, latestSnapshot, writeEnd) -> {\n                                                    if (childProps.thumbnail.isEmpty()) {\n                                                        if (writeEnd <= childProps.size)\n                                                            return Futures.of(latestSnapshot);\n                                                        // update size only\n                                                        return updatedChild.updateSize(committer, writeEnd, network);\n                                                    }\n                                                    return updatedChild.getInputStream(latestSnapshot.get(updatedChild.writer()).props, network, crypto, l -> {})\n                                                            .thenCompose(is -> updatedChild.recalculateThumbnail(\n                                                                latestSnapshot, committer, filename, is, isHidden,\n                                                                updatedChild.getSize(), updatedChild.props.created, network, (WritableAbsoluteCapability)updatedChild.pointer.capability,\n                                                                updatedChild.getFileProperties().streamSecret));\n                                                };\n\n                                                if (truncateExisting && endIndex < childProps.size) {\n                                                    return child.truncate(current, committer, endIndex, network, crypto).thenCompose( updatedSnapshot ->\n                                                        getUpdated(updatedSnapshot, network).thenCompose( updatedParent ->\n                                                                child.getUpdated(updatedSnapshot, network).thenCompose( updatedChild ->\n                                                                    updatedParent.updateExistingChild(updatedSnapshot, committer, updatedChild, fileData,\n                                                                        startIndex, endIndex, network, crypto, monitor)\n                                                                            .thenCompose(latestSnapshot ->  updatePropsIfNecessary.apply(updatedChild, latestSnapshot, endIndex)))))\n                                                            .thenApply(s -> new Pair<>(s, Optional.<NamedRelativeCapability>empty()));\n                                                } else {\n                                                    return updateExistingChild(current, committer, child, fileData,\n                                                            startIndex, endIndex, network, crypto, monitor)\n                                                            .thenApply(s -> {\n                                                                monitor.accept(THUMBNAIL_PROGRESS_OFFSET);\n                                                                return new Pair<>(s, Optional.<NamedRelativeCapability>empty());\n                                                            });\n                                                }\n                                            }\n                                            if (startIndex > 0) {\n                                                // TODO if startIndex > 0 prepend with a zero section\n                                                throw new IllegalStateException("Unimplemented!");\n                                            }\n                                            SymmetricKey fileWriteKey = requestedWriteKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey fileKey = requestedBaseKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey dataKey = requestedDataKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey rootRKey = latest.pointer.capability.rBaseKey;\n                                            CryptreeNode dirAccess = latest.pointer.fileAccess;\n                                            SymmetricKey dirParentKey = dirAccess.getParentKey(rootRKey);\n                                            Location parentLocation = getLocation();\n                                            Optional<Bat> parentBat = writableFilePointer().bat;\n                                            LocalDateTime timestamp = LocalDateTime.now();\n                                            return calculateMimeType(fileData, endIndex, filename).thenCompose(mimeType -> fileData.reset()\n                                                    .thenCompose(resetReader -> {\n                                                        Optional<byte[]> actualStreamSecret = streamSecret.isPresent() ?\n                                                                streamSecret :\n                                                                Optional.of(crypto.random.randomBytes(32));\n                                                        FileProperties fileProps = new FileProperties(filename,\n                                                                false, false, mimeType, endIndex,\n                                                                timestamp, timestamp, isHidden, existingThumbnail, actualStreamSecret);\n\n                                                        FileUploader chunks = new FileUploader(filename, resetReader,\n                                                                startIndex, endIndex, fileKey, dataKey, parentLocation, parentBat,\n                                                                dirParentKey, monitor, fileProps, firstChunkMapKey, firstBat);\n\n                                                        SigningPrivateKeyAndPublicHash signer = signingPair();\n                                                        WritableAbsoluteCapability fileWriteCap = new\n                                                                WritableAbsoluteCapability(owner(),\n                                                                signer.publicKeyHash,\n                                                                firstChunkMapKey, firstBat, fileKey,\n                                                                fileWriteKey);\n\n                                                        return chunks.upload(current, committer, network, parentLocation.owner,\n                                                                signer, mirrorBat, crypto.random, crypto.hasher)\n                                                                .thenCompose(cwd -> fileData.reset().thenCompose(resetAgain ->\n                                                                        generateThumbnailAndUpdate(cwd, committer, fileWriteCap, filename, resetAgain,\n                                                                                network, isHidden, mimeType,\n                                                                                endIndex, timestamp, timestamp, actualStreamSecret, monitor)))\n                                                                .thenApply(s -> new Pair<>(s, Optional.of(new NamedRelativeCapability(filename, writableFilePointer().relativise(fileWriteCap)))));\n                                                    }));\n                                        })\n                        )\n                );\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> calculateAndUpdateThumbnail(NetworkAccess network, Crypto crypto) {\n        return network.synchronizer.applyComplexComputation(owner(), signingPair(),\n                (latestSnapshot, committer) -> getInputStream(latestSnapshot.get(writer()).props, network, crypto, l -> {})\n                        .thenCompose(is -> recalculateThumbnail(\n                                latestSnapshot, committer, getName(), is, props.isHidden,\n                                getSize(), props.created, network, (WritableAbsoluteCapability)pointer.capability,\n                                getFileProperties().streamSecret))\n                        .thenApply(res -> new Pair<>(res, true))\n                        .exceptionally(ex -> new Pair<>(latestSnapshot, false))\n        ).thenApply(p -> p.right);\n    }\n\n    private CompletableFuture<Snapshot> recalculateThumbnail(Snapshot snapshot, Committer committer, String filename,\n                                                             AsyncReader fileData, boolean isHidden, long fileSize,\n                                                             LocalDateTime createdDateTime, NetworkAccess network,\n                                                             WritableAbsoluteCapability fileWriteCap, Optional<byte[]> streamSecret\n    ) {\n        return fileData.reset()\n                .thenCompose(fileData2 -> calculateMimeType(fileData2, fileSize, filename)\n                        .thenCompose(mimeType -> fileData.reset()\n                                .thenCompose(resetAgain ->\n                                    generateThumbnailAndUpdate(snapshot, committer, fileWriteCap, filename, resetAgain,\n                                            network, isHidden, mimeType, fileSize, LocalDateTime.now(), createdDateTime, streamSecret, true, x -> {}))));\n    }\n\n    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,\n                                                                   Committer committer,\n                                                                   WritableAbsoluteCapability cap,\n                                                                   String fileName,\n                                                                   AsyncReader fileData,\n                                                                   NetworkAccess network,\n                                                                   Boolean isHidden,\n                                                                   String mimeType,\n                                                                   long fileSize,\n                                                                   LocalDateTime updatedDateTime,\n                                                                   LocalDateTime createdDateTime,\n                                                                   Optional<byte[]> streamSecret,\n                                                                   ProgressConsumer<Long> monitor) {\n        return generateThumbnailAndUpdate(base, committer, cap, fileName, fileData, network, isHidden,\n                mimeType, fileSize, updatedDateTime, createdDateTime, streamSecret, false, monitor);\n    }\n\n    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,\n                                                                   Committer committer,\n                                                                   WritableAbsoluteCapability cap,\n                                                                   String fileName,\n                                                                   AsyncReader fileData,\n                                                                   NetworkAccess network,\n                                                                   Boolean isHidden,\n                                                                   String mimeType,\n                                                                   long fileSize,\n                                                                   LocalDateTime updatedDateTime,\n                                                                   LocalDateTime createdDateTime,\n                                                                   Optional<byte[]> streamSecret,\n                                                                   boolean replaceExistingThumbnail,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.getFile(base, cap, getChildsEntryWriter(), ownername).thenCompose(fileOpt -> {\n            if (replaceExistingThumbnail || fileOpt.get().props.thumbnail.isEmpty()) {\n                return generateThumbnail(network, fileData, (int) Math.min(fileSize, Integer.MAX_VALUE), fileName, mimeType)\n                        .thenCompose(thumbData -> {\n                            if (thumbData.isEmpty())\n                                return Futures.of(base);\n                            FileProperties fileProps = new FileProperties(fileName, false, props.isLink, mimeType, fileSize,\n                                    updatedDateTime, createdDateTime, isHidden, thumbData, streamSecret);\n\n                            return fileOpt.get().updateProperties(base, committer, fileProps, network);\n                        });\n            } else {\n                return Futures.of(base);\n            }\n        }).thenApply(s -> {\n            monitor.accept(THUMBNAIL_PROGRESS_OFFSET);\n            return s;\n        });\n    }\n\n    private CompletableFuture<Snapshot> updateProperties(Snapshot base,\n                                                         Committer committer,\n                                                         FileProperties newProps,\n                                                         NetworkAccess network) {\n        return getPointer().fileAccess.updateProperties(base, committer, writableFilePointer(),\n                getChildsEntryWriter(), newProps, network);\n    }\n\n    private CompletableFuture<Snapshot> addChildPointer(Snapshot current,\n                                                        Committer committer,\n                                                        NamedRelativeCapability childCap,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        return addChildPointers(current, committer, Collections.singletonList(childCap), network, crypto);\n    }\n\n    private CompletableFuture<Snapshot> addChildPointers(Snapshot current,\n                                                        Committer committer,\n                                                        List<NamedRelativeCapability> childCaps,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        return pointer.fileAccess.addChildrenAndCommit(current, committer,\n                childCaps, writableFilePointer(), signingPair(), mirrorBatId(), network, crypto)\n                .thenApply(newBase -> {\n                    setModified();\n                    return newBase;\n                });\n    }\n\n    public CompletableFuture<FileWrapper> appendToChild(String filename,\n                                                        long expectedSize,\n                                                        byte[] fileData,\n                                                        boolean isHidden,\n                                                        Optional<BatId> mirrorBat,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        return getChild(filename, crypto.hasher, network)\n                .thenCompose(child -> child\n                        .flatMap(c -> c.getFileProperties().streamSecret)\n                        .map(secret -> FileProperties.calculateMapKey(secret,\n                                child.get().getLocation().getMapKey(),\n                                child.get().pointer.capability.bat,\n                                child.get().getFileProperties().size, crypto.hasher)\n                                .thenApply(p -> new Triple<>(p.left, p.right, Optional.of(secret))))\n                        .orElseGet(() -> Futures.of(new Triple<>(crypto.random.randomBytes(32),\n                                Optional.of(Bat.random(crypto.random)), Optional.empty())))\n                        .thenCompose(x -> {\n                            long size = child.map(f -> f.getSize()).orElse(0L);\n                            if (size != expectedSize)\n                                throw new IllegalStateException("File has been concurrently modified!");\n                            return uploadFileSection(filename, AsyncReader.build(fileData), isHidden,\n                                    size,\n                                    fileData.length + size,\n                                    child.map(f -> f.getPointer().capability.rBaseKey), true, network, crypto,\n                                    monitor, x.left, x.right, x.middle, mirrorBat);\n                        }));\n    }\n\n    public CompletableFuture<Snapshot> append(byte[] fileData,\n                                              NetworkAccess network,\n                                              Crypto crypto,\n                                              Committer committer,\n                                              ProgressConsumer<Long> progress) {\n        long size = getSize();\n        return overwriteSection(version, committer, AsyncReader.build(fileData), size, size + fileData.length, network, crypto, progress);\n    }\n\n    /**\n     *\n     * @param current\n     * @param committer\n     * @param existingChild\n     * @param fileData\n     * @param inputStartIndex\n     * @param endIndex\n     * @param network\n     * @param crypto\n     * @param monitor\n     * @return The committed root for the parent (this) directory\n     */\n    private CompletableFuture<Snapshot> updateExistingChild(Snapshot current,\n                                                            Committer committer,\n                                                            FileWrapper existingChild,\n                                                            AsyncReader fileData,\n                                                            long inputStartIndex,\n                                                            long endIndex,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            ProgressConsumer<Long> monitor) {\n\n        FileProperties existingProps = existingChild.getFileProperties();\n        String filename = existingProps.name;\n        LOG.info("Overwriting section [" + Long.toHexString(inputStartIndex) + ", " + Long.toHexString(endIndex) + "] of child with name: " + filename);\n\n        return current.withWriter(existingChild.owner(), existingChild.writer(), network)\n                .thenCompose(state ->\n                        existingChild.clean(state, committer, network, crypto)\n                                .thenCompose(pair -> pair.left.overwriteSection(pair.right, committer, fileData,\n                        inputStartIndex, endIndex, network, crypto, monitor)));\n    }\n\n    static boolean isLegalName(String name) {\n        return !name.contains("/") && ! name.equals(".") && ! name.equals("..") && ! name.isEmpty();\n    }\n\n    /**\n     *\n     * @param newFolderName\n     * @param network\n     * @param isSystemFolder\n     * @param crypto\n     * @return An updated version of this directory\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> mkdir(String newFolderName,\n                                                NetworkAccess network,\n                                                boolean isSystemFolder,\n                                                Optional<BatId> mirrorBat,\n                                                Crypto crypto) {\n        return mkdir(newFolderName, network, null, Optional.empty(), isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto);\n    }\n\n    public CompletableFuture<FileWrapper> mkdir(String newFolderName,\n                                                NetworkAccess network,\n                                                SymmetricKey requestedBaseSymmetricKey,\n                                                Optional<Bat> desiredBat,\n                                                boolean isSystemFolder,\n                                                Optional<BatId> mirrorBat,\n                                                Crypto crypto) {\n\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (state, committer) -> mkdir(newFolderName, Optional.ofNullable(requestedBaseSymmetricKey),\n                        Optional.empty(), Optional.empty(), desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), network, crypto, state, committer))\n                .thenCompose(version -> getUpdated(version, network));\n    }\n\n    public CompletableFuture<Snapshot> mkdir(String newFolderName,\n                                             Optional<SymmetricKey> requestedBaseReadKey,\n                                             Optional<SymmetricKey> requestedBaseWriteKey,\n                                             Optional<byte[]> desiredMapKey,\n                                             Optional<Bat> desiredBat,\n                                             boolean isSystemFolder,\n                                             Optional<BatId> mirrorBat,\n                                             NetworkAccess network,\n                                             Crypto crypto,\n                                             Snapshot version,\n                                             Committer committer) {\n\n        if (!this.isDirectory()) {\n            return Futures.errored(new IllegalStateException("Cannot mkdir in a file!"));\n        }\n        if (!isLegalName(newFolderName)) {\n            return Futures.errored(new IllegalStateException("Illegal directory name: " + newFolderName));\n        }\n        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);\n        return hasChildWithName(fullVersion, newFolderName, crypto.hasher, network).thenCompose(hasChild -> {\n            if (hasChild) {\n                return Futures.errored(new IllegalStateException("Child already exists with name: " + newFolderName));\n            }\n            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),\n                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto).thenApply(x -> {\n                setModified();\n                return x;\n            });\n        });\n    }\n\n    /** Get or create a descendant directory\n     *\n     * @param subPath\n     * @param network\n     * @param isSystemFolder\n     * @param crypto\n     * @return\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> getOrMkdirs(Path subPath,\n                                                      NetworkAccess network,\n                                                      boolean isSystemFolder,\n                                                      Optional<BatId> mirrorBat,\n                                                      Crypto crypto) {\n        String finalPath = TrieNode.canonicalise(subPath.toString());\n        List<String> elements = Arrays.asList(finalPath.split("/"));\n        return network.synchronizer.applyComplexComputation(owner(), signingPair(),\n                (state, committer) -> getOrMkdirs(elements, isSystemFolder, mirrorBat, network, crypto, state, committer))\n                .thenApply(p -> p.right);\n    }\n\n    public CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdirs(List<String> subPath,\n                                                                      boolean isSystemFolder,\n                                                                      Optional<BatId> mirrorBat,\n                                                                      NetworkAccess network,\n                                                                      Crypto crypto,\n                                                                      Snapshot version,\n                                                                      Committer committer) {\n        return Futures.reduceAll(subPath, new Pair<>(version, this),\n                (p, name) -> p.right.getOrMkdir(name, Optional.empty(), Optional.empty(), Optional.empty(),\n                                Optional.empty(), isSystemFolder, p.right.mirrorBatId().or(() -> mirrorBat), network, crypto, p.left, committer),\n                (a, b) -> b);\n    }\n\n    private CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdir(String newFolderName,\n                                                                      Optional<SymmetricKey> requestedBaseReadKey,\n                                                                      Optional<SymmetricKey> requestedBaseWriteKey,\n                                                                      Optional<byte[]> desiredMapKey,\n                                                                      Optional<Bat> desiredBat,\n                                                                      boolean isSystemFolder,\n                                                                      Optional<BatId> mirrorBat,\n                                                                      NetworkAccess network,\n                                                                      Crypto crypto,\n                                                                      Snapshot version,\n                                                                      Committer committer) {\n\n        if (! this.isDirectory()) {\n            return Futures.errored(new IllegalStateException("Cannot mkdir in a file!"));\n        }\n        if (! isLegalName(newFolderName)) {\n            return Futures.errored(new IllegalStateException("Illegal directory name: " + newFolderName));\n        }\n        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);\n        return getChild(fullVersion, newFolderName, network).thenCompose(childOpt -> {\n            if (childOpt.isPresent()) {\n                return Futures.of(new Pair<>(fullVersion, childOpt.get()));\n            }\n            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),\n                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBat, crypto).thenCompose(x -> {\n                setModified();\n                return getUpdated(x, network).thenCompose(us -> us.getChild(newFolderName, crypto.hasher, network))\n                        .thenApply(child -> new Pair<>(x, child.get()));\n            });\n        });\n    }\n\n    /**\n     * @param newFilename\n     * @param parent\n     * @param userContext\n     * @return the updated parent\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> rename(String newFilename,\n                                                 FileWrapper parent,\n                                                 Path ourPath,\n                                                 UserContext userContext) {\n        if (! isLegalName(newFilename))\n            return CompletableFuture.completedFuture(parent);\n        if (! parent.isWritable())\n            return Futures.errored(new IllegalStateException("Unable to rename something without write access to the parent!"));\n        CompletableFuture<Optional<FileWrapper>> childExists = parent == null ?\n                CompletableFuture.completedFuture(Optional.empty()) :\n                parent.getDescendentByPath(newFilename, userContext.crypto.hasher, userContext.network);\n        ensureUnmodified();\n        FileProperties currentProps = getFileProperties();\n        setModified();\n        return childExists\n                .thenCompose(existing -> {\n                    if (existing.isPresent())\n                        throw new IllegalStateException("Cannot rename, child already exists with name: " + newFilename);\n\n                    //get current props\n                    RetrievedCapability ourPointer = linkPointer.orElse(pointer);\n                    WritableAbsoluteCapability us = (WritableAbsoluteCapability) ourPointer.capability;\n                    CryptreeNode nodeToUpdate = ourPointer.fileAccess;\n\n                    boolean isDir = this.isDirectory();\n                    boolean isLink = ourPointer.getProperties().isLink;\n                    FileProperties newProps = new FileProperties(newFilename, isDir, isLink,\n                            currentProps.mimeType, currentProps.size,\n                            currentProps.modified, currentProps.created, currentProps.isHidden,\n                            currentProps.thumbnail, currentProps.streamSecret);\n                    SigningPrivateKeyAndPublicHash signer = isLink ? parent.signingPair() : signingPair();\n                    return userContext.network.synchronizer.applyComplexUpdate(owner(), signer,\n                            (s, committer) -> nodeToUpdate.updateProperties(s, committer, us,\n                                    entryWriter, newProps, userContext.network)\n                                    .thenCompose(updated -> parent.updateChildLinks(updated, committer,\n                                            Arrays.asList(new Pair<>(us, new NamedAbsoluteCapability(newFilename, us))),\n                                            userContext.network, userContext.crypto.random, userContext.crypto.hasher))\n                                    .thenCompose(v -> userContext.isSecretLink() ? Futures.of(v) :\n                                            userContext.sharedWithCache.rename(ourPath,\n                                                    ourPath.getParent().resolve(newFilename), v, committer, userContext.network))\n                    ).thenCompose(newVersion -> parent.getUpdated(newVersion, userContext.network));\n                });\n    }\n\n    public CompletableFuture<Boolean> setProperties(FileProperties updatedProperties,\n                                                    Hasher hasher,\n                                                    NetworkAccess network,\n                                                    Optional<FileWrapper> parent) {\n        setModified();\n        String newName = updatedProperties.name;\n        if (!isLegalName(newName)) {\n            return Futures.errored(new IllegalArgumentException("Illegal file name: " + newName));\n        }\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, comitter) -> (! parent.isPresent() ?\n                        CompletableFuture.completedFuture(s) :\n                        s.withWriter(owner(), parent.get().writer(), network)\n                ).thenCompose(withParent -> parent.get().hasChildWithName(withParent, newName, hasher, network))\n                        .thenCompose(hasChild -> ! hasChild ?\n                                CompletableFuture.completedFuture(true) :\n                                parent.get().getChildrenCapabilities(hasher, network)\n                                        .thenApply(childCaps -> {\n                                            if (! childCaps.stream()\n                                                    .map(l -> new ByteArrayWrapper(l.cap.getMapKey()))\n                                                    .collect(Collectors.toSet())\n                                                    .contains(new ByteArrayWrapper(pointer.capability.getMapKey())))\n                                                throw new IllegalStateException("Cannot rename to same name as an existing file");\n                                            return true;\n                                        })).thenCompose(x -> {\n                            CryptreeNode fileAccess = pointer.fileAccess;\n                            return fileAccess.updateProperties(s, comitter, writableFilePointer(),\n                                    entryWriter, updatedProperties, network);\n                        }))\n                .thenApply(fa -> true);\n    }\n\n    @JsMethod\n    public AbsoluteCapability readOnlyPointer() {\n        return pointer.capability.readOnly();\n    }\n\n    public WritableAbsoluteCapability writableFilePointer() {\n        if (! isWritable())\n            throw new IllegalStateException("File is not writable!");\n        return (WritableAbsoluteCapability) pointer.capability;\n    }\n\n    public SigningPrivateKeyAndPublicHash signingPair() {\n        if (! isWritable())\n            throw new IllegalStateException("File is not writable!");\n        return pointer.capability.wBaseKey\n                .map(w -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, w, entryWriter))\n                .orElseGet(entryWriter::get);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> moveTo(FileWrapper target, FileWrapper parent, Path ourPath, UserContext context) {\n        return copyTo(target, context)\n                .thenCompose(fw -> remove(parent, ourPath, context))\n                .thenApply(newAccess -> true);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> copyTo(FileWrapper target, UserContext context) {\n        ensureUnmodified();\n        NetworkAccess network = context.network;\n        Crypto crypto = context.crypto;\n        if (! target.isDirectory()) {\n            return Futures.errored(new IllegalStateException("CopyTo target " + target + " must be a directory"));\n        }\n\n        return context.network.synchronizer.applyComplexUpdate(target.owner(), target.signingPair(),\n                (version, committer) -> version.withWriter(owner(), writer(), network)\n                        .thenCompose(both -> copyTo(target, this.props.thumbnail, network, crypto, both, committer)))\n                .thenApply(newAccess -> true);\n    }\n\n    private CompletableFuture<Snapshot> copyTo(FileWrapper target,\n                                              Optional<Thumbnail> existingThumbnail,\n                                              NetworkAccess network,\n                                              Crypto crypto,\n                                              Snapshot version,\n                                              Committer committer) {\n        if (! target.isDirectory()) {\n            return Futures.errored(new IllegalStateException("CopyTo target " + target + " must be a directory"));\n        }\n\n        return target.hasChildWithName(version, getFileProperties().name, crypto.hasher, network).thenCompose(childExists -> {\n            if (childExists) {\n                return Futures.errored(new IllegalStateException("CopyTo target " + target + " already has child with name " + getFileProperties().name));\n            }\n            if (isDirectory()) {\n                byte[] newMapKey = crypto.random.randomBytes(32);\n                Optional<Bat> newBat = Optional.of(Bat.random(crypto.random));\n                SymmetricKey newBaseR = SymmetricKey.random();\n                SymmetricKey newBaseW = SymmetricKey.random();\n                WritableAbsoluteCapability newCap = ((WritableAbsoluteCapability)target.getPointer().capability)\n                        .withMapKey(newMapKey, newBat)\n                        .withBaseKey(newBaseR)\n                        .withBaseWriteKey(newBaseW);\n                return withVersion(this.version.mergeAndOverwriteWith(version))\n                        .getChildren(version, crypto.hasher, network).thenCompose(children ->\n                        target.mkdir(getName(), Optional.of(newBaseR), Optional.of(newBaseW), Optional.of(newMapKey),\n                                newBat, getFileProperties().isHidden, target.mirrorBatId(), network, crypto, version, committer)\n                                .thenCompose(versionWithDir ->\n                                        network.getFile(versionWithDir, newCap, target.getChildsEntryWriter(), target.ownername)\n                                                .thenCompose(subTargetOpt -> {\n                                                    FileWrapper newTarget = subTargetOpt.get();\n                                                    return Futures.reduceAll(children, versionWithDir,\n                                                            (s, child) -> newTarget.getUpdated(s, network)\n                                                                    .thenCompose(updated ->\n                                                                            child.copyTo(updated, existingThumbnail, network, crypto, s, committer)),\n                                                            (a, b) -> a.merge(b));\n                                                })));\n            } else {\n                return version.withWriter(owner(), writer(), network).thenCompose(snapshot ->\n                        getInputStream(snapshot.get(writer()).props, network, crypto, x -> {})\n                                .thenCompose(stream -> target.uploadFileSection(snapshot, committer,\n                                                getName(), stream, existingThumbnail, false, 0, getSize(),\n                                                Optional.empty(), Optional.empty(), Optional.empty(), false, false, false, network, crypto, x -> {},\n                                                crypto.random.randomBytes(32), Optional.empty(),\n                                                Optional.of(Bat.random(crypto.random)), target.mirrorBatId())\n                                        .thenCompose(p -> p.right.isEmpty() ?\n                                                Futures.of(p.left) :\n                                                target.addChildPointer(p.left, committer, p.right.get(), network, crypto))));\n            }\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> hasChild(String fileName, Hasher hasher, NetworkAccess network) {\n        if (!isLegalName(fileName)) {\n            return Futures.errored(new IllegalArgumentException("Illegal file/directory name: " + fileName));\n        }\n        return this.hasChildWithName(version, fileName, hasher, network).thenApply(childExists -> childExists);\n    }\n\n    /**\n     * Move this file/dir and subtree to a new signing key pair.\n     * @param signer\n     * @param parent\n     * @param network\n     * @return The updated version of this file/dir and its parent\n     */\n    public CompletableFuture<Pair<FileWrapper, FileWrapper>> changeSigningKey(SigningPrivateKeyAndPublicHash signer,\n                                                                              FileWrapper parent,\n                                                                              NetworkAccess network,\n                                                                              SafeRandom random,\n                                                                              Hasher hasher) {\n        ensureUnmodified();\n        WritableAbsoluteCapability cap = (WritableAbsoluteCapability)getPointer().capability;\n        SymmetricLinkToSigner signerLink = SymmetricLinkToSigner.fromPair(cap.wBaseKey.get(), signer);\n        CryptreeNode fileAccess = getPointer().fileAccess;\n\n        RelativeCapability newParentLink = new RelativeCapability(Optional.of(parent.writer()),\n                parent.getLocation().getMapKey(), parent.writableFilePointer().bat, parent.getParentKey(), Optional.empty());\n        CryptreeNode newFileAccess = fileAccess\n                .withWriterLink(cap.rBaseKey, signerLink)\n                .withParentLink(getParentKey(), newParentLink);\n        WritableAbsoluteCapability ourNewCap = cap.withSigner(signer.publicKeyHash);\n        RetrievedCapability newRetrievedCapability = new RetrievedCapability(ourNewCap, newFileAccess);\n\n        // create the new signing subspace move subtree to it\n        PublicKeyHash owner = owner();\n\n        network.synchronizer.putEmpty(owner, signer.publicKeyHash);\n        return network.synchronizer.applyComplexUpdate(owner, signer, (version, committer) -> IpfsTransaction.call(owner,\n                tid -> network.uploadChunk(version, committer, newFileAccess, owner, getPointer().capability.getMapKey(), signer, tid)\n                        .thenCompose(newVersion -> copyAllChunks(false, cap, signer, hasher, network, newVersion, committer))\n                        .thenCompose(copiedVersion -> copiedVersion.withWriter(owner, parent.writer(), network))\n                        .thenCompose(withParent -> parent.getPointer().fileAccess\n                                .updateChildLink(withParent, committer, parent.writableFilePointer(),\n                                        parent.signingPair(),\n                                        getPointer(),\n                                        newRetrievedCapability, network, random, hasher))\n                        .thenCompose(updatedParentVersion -> deleteAllChunks(cap, signingPair(), tid, hasher, network,\n                                updatedParentVersion, committer)),\n                network.dhtClient)\n        ).thenCompose(finalVersion -> parent.getUpdated(finalVersion, network)\n                .thenCompose(updatedParent -> network.getFile(finalVersion, ourNewCap, Optional.of(signer), ownername)\n                .thenApply(updatedUs -> new Pair<>(updatedUs.get(), updatedParent))));\n    }\n\n    /** This copies all the cryptree nodes from one signing key to another for a file or subtree\n     *\n     * @param includeFirst\n     * @param currentCap\n     * @param targetSigner\n     * @param network\n     * @return\n     */\n    private static CompletableFuture<Snapshot> copyAllChunks(boolean includeFirst,\n                                                             AbsoluteCapability currentCap,\n                                                             SigningPrivateKeyAndPublicHash targetSigner,\n                                                             Hasher hasher,\n                                                             NetworkAccess network,\n                                                             Snapshot initialVersion,\n                                                             Committer committer) {\n\n        return initialVersion.withWriter(currentCap.owner, currentCap.writer, network)\n                .thenCompose(version -> network.getMetadata(version.get(currentCap.writer).props, currentCap)\n                        .thenCompose(mOpt -> {\n                            if (! mOpt.isPresent()) {\n                                return CompletableFuture.completedFuture(version);\n                            }\n                            return (includeFirst ?\n                                    IpfsTransaction.call(currentCap.owner,\n                                            tid -> network.addPreexistingChunk(mOpt.get(), currentCap.owner, currentCap.getMapKey(),\n                                                    targetSigner, tid, version, committer), network.dhtClient) :\n                                    CompletableFuture.completedFuture(version))\n                                    .thenCompose(updated -> {\n                                        CryptreeNode chunk = mOpt.get();\n                                        Optional<byte[]> streamSecret = chunk.getProperties(chunk\n                                                .getParentKey(currentCap.rBaseKey)).streamSecret;\n                                        return chunk.getNextChunkLocation(currentCap.rBaseKey,\n                                                streamSecret, currentCap.getMapKey(), currentCap.bat, hasher)\n                                                .thenCompose(nextChunkMapKeyAndBat ->\n                                                        copyAllChunks(true, currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right),\n                                                                targetSigner, hasher, network, updated, committer));\n                                    })\n                                    .thenCompose(updatedVersion -> {\n                                        if (! mOpt.get().isDirectory())\n                                            return CompletableFuture.completedFuture(updatedVersion);\n                                        return mOpt.get().getDirectChildrenCapabilities(currentCap, updatedVersion, network)\n                                                .thenCompose(childCaps ->\n                                                        Futures.reduceAll(childCaps,\n                                                                updatedVersion,\n                                                                (v, cap) -> copyAllChunks(true, cap.cap,\n                                                                        targetSigner, hasher, network, v, committer),\n                                                                (x, y) -> y));\n                                    });\n                        }));\n    }\n\n    public static CompletableFuture<Snapshot> deleteAllChunks(WritableAbsoluteCapability currentCap,\n                                                              SigningPrivateKeyAndPublicHash signer,\n                                                              TransactionId tid,\n                                                              Hasher hasher,\n                                                              NetworkAccess network,\n                                                              Snapshot version,\n                                                              Committer committer) {\n        return version.withWriter(currentCap.owner, currentCap.writer, network)\n                .thenCompose(current -> network.getMetadata(current.get(currentCap.writer).props, currentCap)\n                        .thenCompose(mOpt -> {\n                            if (! mOpt.isPresent()) {\n                                return CompletableFuture.completedFuture(current);\n                            }\n                            CryptreeNode chunk = mOpt.get();\n                            SigningPrivateKeyAndPublicHash ourSigner = chunk\n                                    .getSigner(currentCap.rBaseKey, currentCap.wBaseKey.get(), Optional.of(signer));\n                            FileProperties props = chunk.getProperties(chunk\n                                    .getParentKey(currentCap.rBaseKey));\n                            Optional<byte[]> streamSecret = props.streamSecret;\n\n                            boolean normalFile = ! chunk.isDirectory() && streamSecret.isPresent();\n                            return (normalFile ?\n                                    deleteFileChunks(props.streamSecret.get(), props.chunkCount(), currentCap, ourSigner, tid, hasher, network, current, committer) :\n                                    network.deleteChunk(current, committer, chunk, currentCap.owner,\n                                                    currentCap.getMapKey(), ourSigner, tid)\n                                            .thenCompose(deletedVersion -> {\n                                                return chunk.getNextChunkLocation(currentCap.rBaseKey, streamSecret,\n                                                        currentCap.getMapKey(), currentCap.bat, hasher).thenCompose(nextChunkMapKeyAndBat ->\n                                                        deleteAllChunks(currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right), signer, tid, hasher,\n                                                                network, deletedVersion, committer));\n                                            }))\n                                    .thenCompose(updatedVersion -> {\n                                        if (! chunk.isDirectory())\n                                            return CompletableFuture.completedFuture(updatedVersion);\n                                        return chunk.getDirectChildrenCapabilities(currentCap, updatedVersion, network).thenCompose(childCaps ->\n                                                Futures.reduceAll(childCaps,\n                                                        updatedVersion,\n                                                        (v, cap) -> deleteAllChunks((WritableAbsoluteCapability) cap.cap, signer,\n                                                                tid, hasher, network, v, committer),\n                                                        (x, y) -> y));\n                                    })\n                                    .thenCompose(s -> removeSigningKey(currentCap.writer, signer, currentCap.owner, network, s, committer));\n                        }));\n    }\n\n    private static CompletableFuture<Snapshot> deleteFileChunks(byte[] streamSecret,\n                                                                int nChunks,\n                                                                WritableAbsoluteCapability startCap,\n                                                                SigningPrivateKeyAndPublicHash ourSigner,\n                                                                TransactionId tid,\n                                                                Hasher hasher,\n                                                                NetworkAccess network,\n                                                                Snapshot current,\n                                                                Committer c) {\n        return getAllChunkLocations(startCap.getMapKey(), streamSecret, nChunks, hasher)\n                .thenCompose(labels -> network.deleteAllChunksIfPresent(current, c, startCap.owner, ourSigner, labels, tid));\n    }\n\n    private static CompletableFuture<List<byte[]>> getAllChunkLocations(byte[] first, byte[] streamSecret, int nChunks, Hasher h) {\n        List<byte[]> res = new ArrayList<>(nChunks);\n        res.add(first);\n        return Futures.reduceAll(IntStream.range(1, nChunks).mapToObj(i -> i), res,\n                (labels, i) -> FileProperties.calculateNextMapKey(streamSecret, labels.get(labels.size() - 1), Optional.empty(), h)\n                        .thenApply(next -> {\n                            labels.add(next.left);\n                            return labels;\n                        }),\n                (a, b) -> b);\n    }\n\n    /**\n     * @param parent\n     * @param userContext\n     * @return updated parent\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> remove(FileWrapper parent, Path ourPath, UserContext userContext) {\n        NetworkAccess network = userContext.network;\n        Hasher hasher = userContext.crypto.hasher;\n        ensureUnmodified();\n        if (! pointer.capability.isWritable())\n            return Futures.errored(new IllegalStateException("Cannot delete file without write access to it"));\n\n        boolean writableParent = parent.isWritable();\n        parent.setModified();\n        network.disableCommits();\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (version, c) -> {\n                    return (writableParent ? version.withWriter(owner(), parent.writer(), network)\n                            .thenCompose(v2 -> parent.pointer.fileAccess\n                                    .removeChildren(v2, c, Arrays.asList(getPointer().capability), parent.writableFilePointer(),\n                                            parent.entryWriter, network, userContext.crypto.random, hasher)) :\n                            Futures.of(version))\n                            .thenCompose(v -> IpfsTransaction.call(owner(),\n                                    tid -> FileWrapper.deleteAllChunks(\n                                            isLink() ?\n                                                    (WritableAbsoluteCapability) getLinkPointer().capability :\n                                                    writableFilePointer(),\n                                            writableParent ?\n                                                    parent.signingPair() :\n                                                    signingPair(), tid, hasher, network, v, c), network.dhtClient)\n                                    .thenCompose(s -> userContext.isSecretLink() ? Futures.of(s) :\n                    userContext.sharedWithCache.clearSharedWith(ourPath, s, c, network)));\n                })\n                .thenCompose(s -> parent.getUpdated(s, network));\n    }\n\n    public static CompletableFuture<Snapshot> removeSigningKey(PublicKeyHash signerToRemove,\n                                                               SigningPrivateKeyAndPublicHash parentSigner,\n                                                               PublicKeyHash owner,\n                                                               NetworkAccess network,\n                                                               Snapshot current,\n                                                               Committer committer) {\n        PublicKeyHash parentWriter = parentSigner.publicKeyHash;\n        if (parentWriter.equals(signerToRemove))\n            return CompletableFuture.completedFuture(current);\n\n        return current.withWriter(owner, parentWriter, network)\n                .thenCompose(s -> s.get(parentSigner).props\n                        .removeOwnedKey(owner, parentSigner, signerToRemove, network.dhtClient, network.hasher)\n                        .thenCompose(removed -> IpfsTransaction.call(\n                                owner,\n                                tid -> committer.commit(owner, parentSigner, removed, s.get(parentSigner), tid),\n                                network.dhtClient))\n                        .thenApply(committed -> s.withVersion(parentWriter, committed.get(parentWriter))));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, getFileProperties().size, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(WriterData version,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   ProgressConsumer<Long> monitor) {\n        return getInputStream(version, network, crypto, getFileProperties().size, monitor);\n    }\n\n    @JsMethod\n    public CompletableFuture<? extends AsyncReader> getBufferedInputStream(NetworkAccess network,\n                                                                           Crypto crypto,\n                                                                           int fileSizeHi,\n                                                                           int fileSizeLow,\n                                                                           int bufferChunks,\n                                                                           ProgressConsumer<Long> monitor) {\n        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);\n        return getInputStream(network, crypto, fileSizeHi, fileSizeLow, monitor)\n                .thenApply(r -> new BufferedAsyncReader(r, bufferChunks, fileSize));\n    }\n\n    @JsMethod\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   int fileSizeHi,\n                                                                   int fileSizeLow,\n                                                                   ProgressConsumer<Long> monitor) {\n        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, fileSize, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   long fileSize,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()).props, network, crypto, fileSize, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(WriterData version,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   long fileSize,\n                                                                   ProgressConsumer<Long> monitor) {\n        ensureUnmodified();\n        if (pointer.fileAccess.isDirectory())\n            throw new IllegalStateException("Cannot get input stream for a directory!");\n        CryptreeNode fileAccess = pointer.fileAccess;\n        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)\n                .thenCompose(retriever ->\n                        retriever.getFile(version, network, crypto, pointer.capability, props.streamSecret,\n                                fileSize, fileAccess.committedHash(), monitor));\n    }\n\n    private CompletableFuture<FileRetriever> getRetriever(Hasher hasher) {\n        if (pointer.fileAccess.isDirectory())\n            throw new IllegalStateException("Cannot get input stream for a directory!");\n        return pointer.fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, hasher);\n    }\n\n    @JsMethod\n    public String getBase64Thumbnail() {\n        Optional<Thumbnail> thumbnail = props.thumbnail;\n        if (thumbnail.isPresent()) {\n            Thumbnail thumb = thumbnail.get();\n            String base64Data = Base64.getEncoder().encodeToString(thumb.data);\n            if (thumb.mimeType.equals("image/webp"))\n                return "data:image/webp;base64," + base64Data;\n            if (thumb.mimeType.equals("image/jpeg"))\n                return "data:image/jpeg;base64," + base64Data;\n            if (thumb.mimeType.equals("image/png"))\n                return "data:image/png;base64," + base64Data;\n            throw new IllegalStateException("Unknown thumbnail mimetype: " + thumb.mimeType);\n        } else {\n            return "";\n        }\n    }\n\n    @JsMethod\n    public FileProperties getFileProperties() {\n        ensureUnmodified();\n        return props;\n    }\n\n    @JsMethod\n    public String getName() {\n        return getFileProperties().name;\n    }\n\n    public long getSize() {\n        return getFileProperties().size;\n    }\n\n    public String toString() {\n        return getFileProperties().name;\n    }\n\n    public static FileWrapper createRoot(TrieNode root) {\n        return new FileWrapper(Optional.of(root), null, Optional.empty(), Optional.empty(), null, new Snapshot(new HashMap<>()));\n    }\n\n    public static Optional<Thumbnail> generateVideoThumbnail(byte[] videoBlob) {\n        File tempFile = null;\n        try {\n            tempFile = File.createTempFile(UUID.randomUUID().toString(), ".mp4");\n            Files.write(tempFile.toPath(), videoBlob, StandardOpenOption.WRITE);\n            return VideoThumbnail.create(tempFile.getAbsolutePath(), THUMBNAIL_SIZE, THUMBNAIL_SIZE);\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n        } finally {\n            if(tempFile != null) {\n                try {\n                    Files.delete(tempFile.toPath());\n                }catch(IOException ioe){\n\n                }\n            }\n        }\n        return Optional.empty();\n    }\n\n    private static Optional<Thumbnail> convertFromBase64(String base64Url) {\n        String base64data = base64Url.substring(base64Url.indexOf(",") + 1);\n        byte[] data = Base64.getDecoder().decode(base64data);\n        if (data.length == 0)\n            return Optional.empty();\n        if (base64Url.startsWith("data:image/jpeg;base64,"))\n            return Optional.of(new Thumbnail("image/jpeg", data));\n        if (base64Url.startsWith("data:image/webp;base64,"))\n            return Optional.of(new Thumbnail("image/webp", data));\n        throw new IllegalStateException("Unknown image type for generated thumbnail!");\n    }\n\n    private CompletableFuture<Optional<Thumbnail>> generateThumbnail(NetworkAccess network, AsyncReader fileData, int fileSize, String filename, String mimeType) {\n        CompletableFuture<Optional<Thumbnail>> fut = new CompletableFuture<>();\n        if (fileSize > MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE) {\n            if (mimeType.startsWith("image")) {\n                if (network.isJavascript()) {\n                    thumbnail.generateThumbnail(fileData, fileSize, filename).thenAccept(base64Str -> {\n                        fut.complete(convertFromBase64(base64Str));\n                    });\n                } else {\n                    byte[] bytes = new byte[fileSize];\n                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {\n                        fut.complete(ThumbnailGenerator.get().generateThumbnail(bytes));\n                    }).exceptionally(t -> {\n                        fut.complete(Optional.empty());\n                        return null;\n                    });\n                }\n            } else if (mimeType.startsWith("video")) {\n                if (network.isJavascript()) {\n                    thumbnail.generateVideoThumbnail(fileData, fileSize, filename, mimeType).thenAccept(base64Str -> {\n                        if(base64Str == null) {\n                            fut.complete(Optional.empty());\n                        }\n                        fut.complete(convertFromBase64(base64Str));\n                    });\n                } else {\n                    // TODO find a cross platform way to generate (streaming) video thumbnails in Java\n                    fut.complete(Optional.empty());\n//                    byte[] bytes = new byte[fileSize];\n//                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {\n//                        fut.complete(generateVideoThumbnail(bytes));\n//                    }).exceptionally(t -> {\n//                        fut.complete(Optional.empty());\n//                        return null;\n//                    });\n                }\n            } else if (mimeType.startsWith("audio/mpeg")) {\n                byte[] mp3Data = new byte[fileSize];\n                fileData.readIntoArray(mp3Data, 0, fileSize).thenAccept(read -> {\n                    try {\n                        Mp3CoverImage mp3CoverImage = Mp3CoverImage.extractCoverArt(mp3Data);\n                        if (mp3CoverImage.imageData == null) {\n                            fut.complete(Optional.empty());\n                        } else {\n                            if (network.isJavascript()) {\n                                AsyncReader.ArrayBacked imageBlob = new AsyncReader.ArrayBacked(mp3CoverImage.imageData);\n                                thumbnail.generateThumbnail(imageBlob, mp3CoverImage.imageData.length, filename)\n                                        .thenAccept(base64Str -> {\n                                            fut.complete(convertFromBase64(base64Str));\n                                        });\n                            } else {\n                                fut.complete(ThumbnailGenerator.get().generateThumbnail(mp3CoverImage.imageData));\n                            }\n                        }\n                    } catch(Exception ex) {\n                        fut.complete(Optional.empty());\n                    }\n                });\n            } else {\n                fut.complete(Optional.empty());\n            }\n        } else {\n            fut.complete(Optional.empty());\n        }\n        return fut;\n    }\n\n    private static CompletableFuture<String> getFileType(AsyncReader imageBlob, String filename) {\n        CompletableFuture<String> result = new CompletableFuture<>();\n        byte[] data = new byte[MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE];\n        imageBlob.readIntoArray(data, 0, data.length).thenAccept(numBytesRead -> {\n            imageBlob.reset().thenAccept(resetResult -> {\n                if (numBytesRead < data.length) {\n                    result.complete("");\n                } else {\n                    String mimeType = MimeTypes.calculateMimeType(data, filename);\n                    result.complete(mimeType);\n                }\n            });\n        });\n        return result;\n    }\n\n    public static CompletableFuture<String> calculateMimeType(AsyncReader data, long fileSize, String filename) {\n        byte[] header = new byte[(int) Math.min(fileSize, MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE)];\n        return data.readIntoArray(header, 0, header.length)\n                .thenApply(read -> MimeTypes.calculateMimeType(header, filename));\n    }\n}\n', 'message': 'Fix FileWrapper.getDescendentByPath on Windows\n', 'author_name': 'ian', 'lang': 'java'}