<commit_before>if (typeof module !== "undefined")
    var nacl = require("./nacl");
if (typeof module !== "undefined")
    var erasure = require("./erasure");

function humanSort(l,r) {
                            const comp = l.toLowerCase().localeCompare(r.toLowerCase());
                            return comp != 0 ? comp : l.localeCompare(r);
};
// API for the User interface to use
/////////////////////////////

/////////////////////////////
// UserPublicKey methods
function UserPublicKey(publicSignKey, publicBoxKey) {
    this.pSignKey = publicSignKey; // 32 bytes
    this.pBoxKey = publicBoxKey; // 32 bytes

    // ((err, publicKeyString) -> ())
    this.getPublicKeys = function() {
        var tmp = new Uint8Array(this.pSignKey.length + this.pBoxKey.length);
        tmp.set(this.pSignKey, 0);
        tmp.set(this.pBoxKey, this.pSignKey.length);
        return tmp;
    }
    
    // (Uint8Array, User, (nonce, cipher) -> ())
    this.encryptMessageFor = function(input, us) {
        var nonce = createNonce();
        return concat(nacl.box(input, nonce, this.pBoxKey, us.sBoxKey), nonce);
    }
    
    // Uint8Array => boolean
    this.unsignMessage = function(sig) {
        return nacl.sign.open(sig, this.pSignKey);
    }
}
//Uint8Array => UserPublicKey
UserPublicKey.fromPublicKeys = function(both) {
    var pSign = slice(both, 0, 32);
    var pBox = slice(both, 32, 64);
    return new UserPublicKey(pSign, pBox);
}

UserPublicKey.HASH_BYTES = 34;
// Uint8Array => Uint8Array
UserPublicKey.hash = function(arr) {
    const hash = sha256(arr);
    const multihash = new Uint8Array(hash.length+2);
    multihash[0] = 0x12;
    multihash[1] = 0x20;
    for(var i=0; i < hash.length; i++) multihash[i+2] = hash[i];

    return multihash;
}

function createNonce(){
    return window.nacl.randomBytes(24);
}

/////////////////////////////
// User methods
// (string, string, (User -> ())
function generateKeyPairs(username, password, cb) {
    var hash = sha256(nacl.util.decodeUTF8(password));
    var salt = nacl.util.decodeUTF8(username)
    
    return new Promise(function(resolve, reject) {
        scrypt(hash, salt, 17, 8, 96, 1000, function(keyBytes) {
            var bothBytes = nacl.util.decodeBase64(keyBytes);
            var signBytes = bothBytes.subarray(0, 32);
            var boxBytes = bothBytes.subarray(32, 64);
	    var rootKeyBytes = bothBytes.subarray(64, 96);
            resolve({
		user:new User(nacl.sign.keyPair.fromSeed(signBytes), nacl.box.keyPair.fromSecretKey(new Uint8Array(boxBytes))),
		root:new SymmetricKey(new Uint8Array(rootKeyBytes))
	    });
        }, 'base64');
    });
}

function User(signKeyPair, boxKeyPair) {
    UserPublicKey.call(this, signKeyPair.publicKey, boxKeyPair.publicKey);
    this.sSignKey = signKeyPair.secretKey; // 64 bytes
    this.sBoxKey = boxKeyPair.secretKey; // 32 bytes
    
    // (Uint8Array, (nonce, sig) => ())
    this.hashAndSignMessage = function(input, cb) {
        signMessage(this.hash(input), cb);
    }
    
    // (Uint8Array => Uint8array)
    this.signMessage = function(input) {
        return nacl.sign(input, this.sSignKey);
    }
    
    // (Uint8Array, (err, literals) -> ())
    this.decryptMessage = function(cipher, them) {
        var nonce = slice(cipher, cipher.length-24, cipher.length);
        cipher = slice(cipher, 0, cipher.length-24);
        return nacl.box.open(cipher, nonce, them.pBoxKey, this.sBoxKey);
    }

    this.getSecretKeys = function() {
        var tmp = new Uint8Array(this.sSignKey.length + this.sBoxKey.length);
        tmp.set(this.sSignKey, 0);
        tmp.set(this.sBoxKey, this.sSignKey.length);
        return tmp;
    }
}

User.fromEncodedKeys = function(publicKeys, secretKeys) {
    return new User(toKeyPair(slice(publicKeys, 0, 32), slice(secretKeys, 0, 64)), toKeyPair(slice(publicKeys, 32, 64), slice(secretKeys, 64, 96)));
}

User.fromSecretKeys = function(secretKeys) {
    var publicBoxKey = new Uint8Array(32);
    nacl.lowlevel.crypto_scalarmult_base(publicBoxKey, slice(secretKeys, 64, 96))
    return User.fromEncodedKeys(concat(slice(secretKeys, 32, 64), 
                publicBoxKey),
                secretKeys);
}

User.random = function() {
    var secretBoxKey = window.nacl.randomBytes(32);
    var signSeed = window.nacl.randomBytes(32);
    return new User(nacl.sign.keyPair.fromSeed(signSeed), nacl.box.keyPair.fromSecretKey(new Uint8Array(secretBoxKey)));
}

function toKeyPair(pub, sec) {
    return {publicKey:pub, secretKey:sec};
}

/////////////////////////////
// SymmetricKey methods

function SymmetricKey(key) {
    if (key.length != nacl.secretbox.keyLength)
	throw "Invalid symmetric key: "+key;
    this.key = key;

    // (Uint8Array, Uint8Array[24]) => Uint8Array
    this.encrypt = function(data, nonce) {
    return nacl.secretbox(data, nonce, this.key);
    }

    // (Uint8Array, Uint8Array) => Uint8Array
    this.decrypt = function(cipher, nonce) {
    return nacl.secretbox.open(cipher, nonce, this.key);
    }

    // () => Uint8Array
    this.createNonce = function() {
    return nacl.randomBytes(24);
}
}
SymmetricKey.NONCE_BYTES = 24;
SymmetricKey.random = function() {
    return new SymmetricKey(nacl.randomBytes(32));
}

function FileProperties(name, size, modified, attr) {
    this.name = name;
    this.size = size;
    this.modified = modified;
    this.attr = attr; //  | HIDDEN

    this.serialize = function() {
        var buf = new ByteArrayOutputStream();
        buf.writeString(name);
        buf.writeDouble(size);
	buf.writeDouble(modified);
	buf.writeByte(attr);
        return buf.toByteArray();
    }

    this.getSize = function() {
        return size;
    }

    this.getModified = function() {
	return new Date(modified);
    }

    this.isHidden = function() {
	return this.attr & 1 != 0;
    }
}

FileProperties.deserialize = function(raw) {
    const buf = new ByteArrayInputStream(raw);
    var name = buf.readString();
    var size = buf.readDouble();
    var modified = buf.readDouble();
    var attr = buf.readByte();
    return new FileProperties(name, size, modified, attr);
}

function asyncErasureEncode(original, originalBlobs, allowedFailures) {
    var worker = new Worker("scripts/erasure.js");
    var prom = new Promise(function(resolve, reject){
	worker.onmessage = function(e) {
	    var bfrags = e.data;
	    resolve(bfrags);
	};
	worker.postMessage({original:original, originalBlobs:EncryptedChunk.ERASURE_ORIGINAL, allowedFailures:EncryptedChunk.ERASURE_ALLOWED_FAILURES});
    });
    return prom;
}

function Fragment(data) {
    this.data = data;

    this.getData = function() {
        return data;
    }
}
Fragment.SIZE = 128*1024;

function EncryptedChunk(encrypted) {
    this.auth = slice(encrypted, 0, window.nacl.secretbox.overheadLength);
    this.cipher = slice(encrypted, window.nacl.secretbox.overheadLength, encrypted.length);

    this.generateFragments = function() {
        return asyncErasureEncode(this.cipher, EncryptedChunk.ERASURE_ORIGINAL, EncryptedChunk.ERASURE_ALLOWED_FAILURES).then(function(bfrags) {
            var frags = [];
            for (var i=0; i < bfrags.length; i++)
		frags[i] = new Fragment(bfrags[i]);
	    return Promise.resolve(frags);
	});
    }

    this.getAuth = function() {
        return this.auth;
    }

    this.decrypt = function(key, nonce) {
        return key.decrypt(concat(this.auth, this.cipher), nonce);
    }
}
EncryptedChunk.ERASURE_ORIGINAL = 40;
EncryptedChunk.ERASURE_ALLOWED_FAILURES = 10;

function Chunk(data, key) {
    this.data = data;
    this.key = key;
    this.nonce = window.nacl.randomBytes(SymmetricKey.NONCE_BYTES);
    this.mapKey = window.nacl.randomBytes(32);

    this.encrypt = function() {
        return new EncryptedChunk(key.encrypt(data, this.nonce));
    }
}
Chunk.MAX_SIZE = Fragment.SIZE*EncryptedChunk.ERASURE_ORIGINAL
// string, File, SymmetricKey, Location, SymmetricKey -> 
function FileUploader(name, file, key, parentLocation, parentparentKey, setProgressPercentage) {
    this.props = new FileProperties(name, file.size, Date.now(), 0);
    if (key == null) key = SymmetricKey.random();

    // Process and upload chunk by chunk to avoid running out of RAM, in reverse order to build linked list
    this.nchunks = Math.ceil(file.size/Chunk.MAX_SIZE);
    
    this.file = file;
    this.key = key;
    this.parentLocation = parentLocation;
    this.parentparentKey = parentparentKey;
    this.setProgressPercentage = setProgressPercentage;

    this.uploadChunk = function(context, owner, writer, chunkIndex, file, nextLocation) {

	var that = this;
	return new Promise(function(resolve, reject) {
	    console.log("uploading chunk: "+chunkIndex + " of "+file.name);
	    var filereader = new FileReader();
	    filereader.file_name = file.name;
	    filereader.onload = function(){
		const data = new Uint8Array(this.result);
		var chunk = new Chunk(data, key)
		const encryptedChunk = chunk.encrypt();
		encryptedChunk.generateFragments().then(function(fragments){
                    console.log("Uploading chunk with %d fragments\n", fragments.length);
		    context.uploadFragments(fragments, owner, writer, chunk.mapKey, setProgressPercentage).then(function(hashes){
			const retriever = new EncryptedChunkRetriever(chunk.nonce, encryptedChunk.getAuth(), hashes, nextLocation);
			const metaBlob = FileAccess.create(chunk.key, that.props, retriever, parentLocation, parentparentKey);
			context.uploadChunk(metaBlob, owner, writer, chunk.mapKey).then(function() {
			    resolve(new Location(owner, writer, chunk.mapKey));
			});
		    });
		});
	    }
	    filereader.readAsArrayBuffer(file.slice(chunkIndex*Chunk.MAX_SIZE, Math.min((1+chunkIndex)*Chunk.MAX_SIZE, file.size)));
	}).then(function(nextL) {
	    if (chunkIndex > 0)
		return that.uploadChunk(context, owner, writer, chunkIndex-1, file, nextL, setProgressPercentage);
	    return Promise.resolve(nextL);
	});
    }.bind(this);
    
    this.upload = function(context, owner, writer) {
	return this.uploadChunk(context, owner, writer, this.nchunks-1, this.file, null);
    }.bind(this);
}

/////////////////////////////
// Util methods

// byte[] input and return
function encryptBytesToBytes(input, pubKey) {
    return Java.to(encryptUint8ToUint8(input, pubKey), "byte[]");
}

// byte[] cipher and return
function decryptBytesToBytes(cipher, privKey) {
    return Java.to(decryptUint8ToUint8(cipher, privKey), "byte[]");
}

function uint8ArrayToByteArray(arr) {
    return Java.to(arr, "byte[]");
}

function slice(arr, start, end) {
    if (end < start)
        throw "negative slice size! "+start + " -> " + end;
    var r = new Uint8Array(end-start);
    for (var i=start; i < end; i++)
        r[i-start] = arr[i];
    return r;
}

function concat(a, b, c) {
    if (a instanceof Array) {
        var size = 0;
        for (var i=0; i < a.length; i++)
            size += a[i].length;
        var r = new Uint8Array(size);
        var index = 0;
        for (var i=0; i < a.length; i++)
            for (var j=0; j < a[i].length; j++)
                r[index++] = a[i][j];
        return r;
    }
    var r = new Uint8Array(a.length+b.length+(c != null ? c.length : 0));
    for (var i=0; i < a.length; i++)
        r[i] = a[i];
    for (var i=0; i < b.length; i++)
        r[a.length+i] = b[i];
    if (c != null)
        for (var i=0; i < c.length; i++)
            r[a.length+b.length+i] = c[i];
    return r;
}

function arraysEqual(a, b) {
    if (a.length != b.length)
        return false;

    for (var i=0; i < a.length; i++)
        if (a[i] != b[i])
            return false;
    return true;
}

function get(path, onSuccess, onError) {

    var request = new XMLHttpRequest();
    request.open("GET", path);

    request.onreadystatechange=function()
    {
        if (request.readyState != 4)
            return;

        if (request.status == 200) 
            onSuccess(request.response);
        else
            onError(request.status);
    }

    request.send();
}

function getProm(url) {
    return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.responseType = 'arraybuffer';

    req.onload = function() {
        // This is called even on 404 etc
        // so check the status
        if (req.status == 200) {
        resolve(new Uint8Array(req.response));
        }
        else {
        reject(Error(req.statusText));
        }
    };

    req.onerror = function() {
        reject(Error("Network Error"));
    };

    req.send();
    });
}

function post(path, data, onSuccess, onError) {

    var request = new XMLHttpRequest();
    request.open("POST" , path);
    request.responseType = 'arraybuffer';

    request.onreadystatechange=function()
    {
        if (request.readyState != 4)
            return;

        if (request.status == 200) 
            onSuccess(request.response);
        else
            onError(request.status);
    }

    request.send(data);
}

function postProm(url, data) {
    return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('POST', window.location.origin + "/" + url);
    req.responseType = 'arraybuffer';

    req.onload = function() {
        // This is called even on 404 etc
        // so check the status
        if (req.status == 200) {
        resolve(new Uint8Array(req.response));
        }
        else {
        reject(Error(req.statusText));
        }
    };

    req.onerror = function() {
        reject(Error("Network Error"));
    };

    req.send(data);
    });
}

//Java is Big-endian
function readInt(bytes, position) {
    var count = 0;
    for(var i = position; i <  position +4 ; i++)
        count = count << 8 + bytes[i];
    return count;
}

//Java is Big-endian
function writeInt(bytes, position, intValue) {
    intValue |= 0;
    for(var i = position + 3; position <= i ; i--)
        bytes[position] = intValue & 0xff;
        intValue >>= 8;
}

function DHTClient() {
    //
    //put
    //
    this.put = function(valueData, owner) {        
        var arrays = [valueData, owner];
        var buffer = new ByteArrayOutputStream();
        buffer.writeInt(0); // PUT Message
        for (var iArray=0; iArray < arrays.length; iArray++) 
            buffer.writeArray(arrays[iArray]);
        return postProm("dht/put", buffer.toByteArray()).then(function(resBuf){
            var stream = new ByteArrayInputStream(resBuf);
	    var res = stream.readInt();
            if (res == 1) {
		var key = stream.readArray();
		return Promise.resolve(key);
	    }
            return Promise.reject("DHT put failed");
        });
    };
    //
    //get
    //
    this.get = function(keyData) { 
        var buffer = new ByteArrayOutputStream();
        buffer.writeInt(1); // GET Message
        buffer.writeArray(keyData);
        return postProm("dht/get", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("Fragment download failed");
        });
    };
}

function CoreNodeClient() {
    //String -> fn- >fn -> void
    this.getPublicKey = function(username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getPublicKey", buffer.toByteArray()).then(function(raw) {
	    var arr = new ByteArrayInputStream(raw).readArray();
	    var res = arr.length = 0 ? null : UserPublicKey.fromPublicKeys(arr);
	    return Promise.resolve(res);
	});
    };
    
    //UserPublicKey -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.setStaticData = function(owner, signedStaticData) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(signedStaticData);
        return postProm("core/setStaticData", buffer.toByteArray()); 
    };
    
    //String -> fn- >fn -> void
    this.getStaticData = function(owner) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        return postProm("core/getStaticData", buffer.toByteArray());
    };
    
    //Uint8Array -> fn -> fn -> void
    this.getUsername = function(publicKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(publicKey);
        return postProm("core/getUsername", buffer.toByteArray()).then(function(res) {
            const name = new ByteArrayInputStream(res).readString();
            return Promise.resolve(name);
        });
    };
    
    // () -> List<String>
    this.fetchUsernames = function() {
        return getProm("core/getAllUsernamesGzip").then(function(res) {
            const din = new ByteArrayInputStream(res);
	    var res = [];
	    for (;;) {
		var uname = din.readString();
		if (uname == "")
		    break;
		res.push(uname);
	    }
            return Promise.resolve(res);
        });
    };    
    
    //String -> Uint8Array -> Uint8Array -> fn -> fn ->  boolean 
    this.addUsername = function(username, encodedUserKey, signed, staticData) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedUserKey);
        buffer.writeArray(signed);
        buffer.writeArray(staticData);
        return postProm("core/addUsername", buffer.toByteArray()).then(
	    function(res){
		return Promise.resolve(res[0]);
	    });
    };
    
    //Uint8Array -> Uint8Array -> fn -> fn -> void
    this.followRequest = function( target,  encryptedPermission) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(target);
        buffer.writeArray(encryptedPermission);
        return postProm("core/followRequest", buffer.toByteArray()).then(function(res){
	    return Promise.resolve(res[0]==1);
	});
    };
    
    //String -> Uint8Array -> fn -> fn -> void
    this.getFollowRequests = function( user) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(user);
        return postProm("core/getFollowRequests", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var size = buf.readInt();
            var n = buf.readInt();
            var arr = [];
            for (var i=0; i < n; i++) {
                var t = buf.readArray();
                arr.push(t);
            }
            return Promise.resolve(arr);
        });
    };
    
    //Uint8Array -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.removeFollowRequest = function( target,  signedRequest) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(target);
        buffer.writeArray(signedRequest);
        return postProm("core/removeFollowRequest", buffer.toByteArray()).then(function(res) {
            return Promise.resolve(new ByteArrayInputStream(res).readByte() == 1);
        });
    };

    //String -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.banSharingKey = function(username,  encodedSharingPublicKey,  signedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedSharingPublicKey);
        buffer.writeArray(signedHash); 
        return postProm("core/banSharingKey", buffer.toByteArray());
    };

    //Uint8Array -> Uint8Array -> Uint8Array -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.addMetadataBlob = function( owner,  encodedSharingPublicKey, sharingKeySignedPayload) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner);
        buffer.writeArray(encodedSharingPublicKey);
        buffer.writeArray(sharingKeySignedPayload);
        return postProm("core/addMetadataBlob", buffer.toByteArray()).then(function(res) {
            return Promise.resolve(new ByteArrayInputStream(res).readByte() == 1);
        });
    };
    
    //String -> Uint8Array -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.removeMetadataBlob = function( owner,  writer,  mapKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(writer.getPublicKeys());
        buffer.writeArray(writer.signMessage(mapKey));
        return postProm("core/removeMetadataBlob", buffer.toByteArray());
    };

    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.removeUsername = function( username,  userKey,  signedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(userKey);
        buffer.writeArray(signedHash);
        return post("core/removeUsername", buffer.toByteArray());
    };

    //String -> fn -> fn -> void
    this.getSharingKeys = function( username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getSharingKeys", buffer.toByteArray());
    };
    
    //String  -> Uint8Array  -> fn -> fn -> void
    this.getMetadataBlob = function( owner,  encodedSharingKey,  mapKey) {
	var ownerPublic = owner.getPublicKeys();
	for (var i=0; i < ownerPublic.length; i++) {
	    if (ownerPublic[i] != 0)
		break;
	    else if (i == ownerPublic.length-1)
		throw "Invalid zero Owner requested!";
	}
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(ownerPublic);
        buffer.writeArray(encodedSharingKey.getPublicKeys());
        buffer.writeArray(mapKey);
        return postProm("core/getMetadataBlob", buffer.toByteArray());
    };
    
    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.isFragmentAllowed = function( owner,  encodedSharingKey,  mapkey,  hash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(owner);
        buffer.writeArray(encodedSharingKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(hash);
        return postProm("core/isFragmentAllowed", buffer.toByteArray());
    };
    
    //String -> fn -> fn -> void
    this.getQuota = function(user) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(user.getPublicKeys());
        return postProm("core/getQuota", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var quota = buf.readInt() << 32;
            quota += buf.readInt();
            return Promise.resolve(quota);
        });
    };
    
    //String -> fn -> fn -> void
    this.getUsage = function(username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getUsage", buffer.toByteArray());
    };
    
    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.getFragmentHashes = function( username, sharingKey, mapKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("core/getFragmentHashes", buffer.toByteArray());
    };

    //String  -> Uint8Array  -> Uint8Array -> Uint8Array -> [Uint8Array] -> Uint8Array -> fn -> fn -> void
    this.addFragmentHashes = function(username, encodedSharingPublicKey, mapKey, metadataBlob, allHashes, sharingKeySignedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedShaaringPublicKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(metadataBlob);

        buffer.writeInt(allHashes.length);
        for (var iHash=0; iHash  <  allHashes.length; iHash++) 
            buffer.writeArray(allHashes[iHash]);

        buffer.writeArray(sharingKeySignedHash);
        
        return postProm("core/addFragmentHashes", buffer.toByteArray());
    };

    
    this.registerFragmentStorage = function(spaceDonor, address, port, owner, signedKeyPlusHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(spaceDonor.getPublicKeys());
        buffer.writeArray(address);
        buffer.writeInt(port);
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(signedKeyPlusHash);
        return postProm("core/registerFragmentStorage", buffer.toByteArray());
    };
};

function BTree() {
    
    this.put = function(sharingKey, mapKey, value) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(0); // PUT
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(value);
        return postProm("btree/put", buffer.toByteArray()).then(function(resBuf){
            var stream = new ByteArrayInputStream(resBuf);
	    var res = stream.readInt();
            if (res == 1) {
		var key = stream.readArray();
		return Promise.resolve(key);
	    }
            return Promise.reject("BTree put failed");
        });
    }

    this.get = function(sharingKey, mapKey) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(1); // GET
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("btree/get", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("BTree get failed");
        });
    }

    this.remove = function(sharingKey, mapKey) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(2); // DELETE
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("btree/delete", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("BTree delete failed");
        });
    }
}

function UserContext(username, user, rootKey, dhtClient,  corenodeClient) {
    this.username  = username;
    this.user = user;
    this.rootKey = rootKey;
    this.dhtClient = dhtClient;
    this.corenodeClient = corenodeClient;
    this.btree = new BTree();
    this.staticData = []; // array of map entry pairs
    this.rootNode = null;
    this.sharingFolder = null;
    this.usernames = [];

    this.init = function() {
	FileTreeNode.ROOT.clear();
	this.staticData = [];
	var context = this;
	return createFileTree().then(function (rootNode) {
	    context.rootNode = rootNode;
	    return rootNode.getChildren(context).then(function (children) {
		for (var i=0; i < children.length; i++) {
		    if (children[i].getFileProperties().name == context.username)
			return children[i].getChildren().then(function(ourdirs) {
			    for (var j=0; j < ourdirs.length; j++) {
				if (ourdirs[j].getFileProperties().name == "shared") {
				    context.sharingFolder = ourdirs[j];
				    return context.corenodeClient.fetchUsernames().then(function(unames){
					context.usernames = unames;
					return Promise.resolve(true);
				    });
				}
			    }
			    return Promise.reject("Couldn't find shared folder!");
			});
		}
		return Promise.reject("No root directory found!");
	    });
	});
    }.bind(this);

    this.getUsernames= function() {
            return this.usernames;
    }

    this.isRegistered = function() {
        return corenodeClient.getUsername(user.getPublicKeys()).then(function(res){
            return Promise.resolve(username == res);
        });
    }

    this.serializeStatic = function() {
        var buf = new ByteArrayOutputStream();
        buf.writeInt(this.staticData.length);
        for (var i = 0; i < this.staticData.length; i++)
            buf.writeArray(this.staticData[i][1].serializeAndSymmetricallyEncrypt(this.rootKey));
        return buf.toByteArray();
    }

    this.register = function() {
        console.log("registering "+username);
        var rawStatic = this.serializeStatic();
        var signed = user.signMessage(concat(nacl.util.decodeUTF8(username), user.getPublicKeys(), rawStatic));
        return corenodeClient.addUsername(username, user.getPublicKeys(), signed, rawStatic);
    }

    this.createEntryDirectory = function(directoryName) {
        var writer = User.random();
        var rootMapKey = window.nacl.randomBytes(32); // root will be stored under this in the core node
        var rootRKey = SymmetricKey.random();

        // and authorise the writer key
        const rootPointer = new ReadableFilePointer(this.user, writer, rootMapKey, rootRKey);
        const entry = new EntryPoint(rootPointer, this.username, [], []);
        
        return this.addToStaticData(entry).then(function(res) {
            var root = DirAccess.create(rootRKey, new FileProperties(directoryName, 0, Date.now(), 0));
            return this.uploadChunk(root, this.user, writer, rootMapKey).then(function(res) {
		if (res)
		    return Promise.resolve(new RetrievedFilePointer(rootPointer, root));
		return Promise.reject();
	    });
        }.bind(this));
    }

    var getSharingFolder = function() {
	return this.sharingFolder;
    }.bind(this);
    
    var getFriendRoots = function() {
	var ourname = this.username;
	return this.rootNode.getChildren(this).then(function (children) {
	    return Promise.resolve(children.filter(function(froot){return froot.getOwner() != ourname}));
	}.bind(this));
    }.bind(this);

    var getFollowers = function() {
	return getSharingFolder().getChildren(this).then(function(friendFolders){
	    return Promise.resolve(
                        friendFolders.map(function(froot){
                                return froot.getFileProperties().name;
                        }).sort(humanSort));
	});
    }.bind(this);

    var getFollowing = function() {
	var that = this;
	return getFriendRoots().then(function(friendRoots) {
	    return Promise.resolve(friendRoots.map(function(froot){return froot.getOwner()}).filter(function(name){return name != that.username;}));
	});
    }.bind(this)

    var getFollowerRoots = function() {
	return getSharingFolder().getChildren(this).then(function(friendFolders){
	    var res = {};
	    friendFolders.map(function(froot){return res[froot.getFileProperties().name] = froot;});
	    return Promise.resolve(res);
	});
    }.bind(this);

    this.getSocialState = function() {
	return this.getFollowRequests().then(function(pending) {
	    return getFollowerRoots().then(function(followerRoots) {
		return getFriendRoots().then(function(followingRoots) {
		    return Promise.resolve(new SocialState(pending, followerRoots, followingRoots));
		});
	    });
	});
    }.bind(this);
    
    this.sendInitialFollowRequest = function(targetUsername) {
	return this.sendFollowRequest(targetUsername, SymmetricKey.random());
    }

    // FollowRequest, boolean, boolean
    this.sendReplyFollowRequest = function(initialRequest, accept, reciprocate) {
	var context = this;
	var theirUsername = initialRequest.entry.owner;
	// if accept, create directory to share with them, note in entry points (they follow us)
	if (!accept) {
	    // send a null entry and null key (full rejection)
	    //TODO
	    return Promise.resolve(true);
	}
	return getSharingFolder().mkdir(theirUsername, context, initialRequest.key).then(function(friendRoot) {
	    // add a note to our static data so we know who we sent the read access to
	    const entry = new EntryPoint(friendRoot.readOnly(), context.username, [theirUsername], []);
            const targetUser = initialRequest.entry.pointer.owner;
	    return context.addToStaticData(entry).then(function(res) {
		// create a tmp keypair whose public key we can prepend to the request without leaking information
                var tmp = User.random();
		var buf = new ByteArrayOutputStream();
		buf.writeArray(entry.serialize());
		if (! reciprocate) {
		    buf.writeArray(new Uint8Array(0)); // tell them we're not reciprocating
		    var plaintext = buf.toByteArray();
                    var payload = targetUser.encryptMessageFor(plaintext, tmp);

                    return corenodeClient.followRequest(initialRequest.entry.pointer.owner.getPublicKeys(), concat(tmp.pBoxKey, payload));
		}
		// if reciprocate, add entry point to their shared dirctory (we follow them) and them 
		buf.writeArray(initialRequest.entry.pointer.baseKey.key); // tell them we are reciprocating
		var plaintext = buf.toByteArray();
                var payload = targetUser.encryptMessageFor(plaintext, tmp);
		
                return corenodeClient.followRequest(initialRequest.entry.pointer.owner.getPublicKeys(), concat(tmp.pBoxKey, payload)).then(function(res) {
		    return context.addToStaticData(initialRequest.entry);
		});
	    }).then(function(res) {
		// remove original request
		return corenodeClient.removeFollowRequest(context.user.getPublicKeys(), context.user.signMessage(initialRequest.rawCipher));
	    });
	});
    }

    // string, RetrievedFilePointer, SymmetricKey
    this.sendFollowRequest = function(targetUsername, requestedKey) {
	var sharing = getSharingFolder();
	var that = this;
	return sharing.getChildren(this).then(function(children) {
	    var alreadyFollowed = false;
	    for (var i=0; i < children.length; i++)
		if (children[i].getFileProperties().name == targetUsername)
		    alreadyFollowed = true;
	    if (alreadyFollowed)
		return Promise.resolve(false);
	    // check for them not reciprocating
	    return getFollowing().then(function(following) {
		var alreadyFollowed = false;
		for (var i=0; i < following.length; i++)
		    if (following[i] == targetUsername)
			alreadyFollowed = true;
		if (alreadyFollowed)
		    return Promise.resolve(false);

		return that.corenodeClient.getPublicKey(targetUsername).then(function(targetUser) {
		    return sharing.mkdir(targetUsername, that).then(function(friendRoot) {
			
			// add a note to our static data so we know who we sent the read access to
			const entry = new EntryPoint(friendRoot.readOnly(), that.username, [targetUsername], []);
			return that.addToStaticData(entry).then(function(res) {
			    // send details to allow friend to follow us, and optionally let us follow them		
			    // create a tmp keypair whose public key we can prepend to the request without leaking information
			    var tmp = User.random();
			    var buf = new ByteArrayOutputStream();
			    buf.writeArray(entry.serialize());
			    buf.writeArray(requestedKey != null ? requestedKey.key : new Uint8Array(0));
			    var plaintext = buf.toByteArray();
			    var payload = targetUser.encryptMessageFor(plaintext, tmp);
			    
			    return corenodeClient.followRequest(targetUser.getPublicKeys(), concat(tmp.pBoxKey, payload));
			});
		    });
		});
            });
	});
    }.bind(this);

    this.sendWriteAccess = function(targetUser) {
        // create sharing keypair and give it write access
        var sharing = User.random();
        var rootMapKey = window.nacl.randomBytes(32);

        // add a note to our static data so we know who we sent the private key to
        var friendRoot = new ReadableFilePointer(user, sharing, rootMapKey, SymmetricKey.random());
        return this.corenodeClient.getUsername(targetUser.getPublicKeys()).then(function(name) {
            const entry = new EntryPoint(friendRoot, this.username, [], [name]);
            return this.addToStaticData(entry).then(function(res) {
                // create a tmp keypair whose public key we can append to the request without leaking information
                var tmp = User.random();
                var payload = entry.serializeAndEncrypt(tmp, targetUser);
                return corenodeClient.followRequest(targetUser.getPublicKeys(), concat(tmp.pBoxKey, payload));
            });
        }.bind(this));
    }.bind(this);

    this.addToStaticData = function(entry) {
	for (var i=0; i < this.staticData.length; i++)
	    if (this.staticData[i][1].equals(entry))
		return Promise.resolve(true);
        this.staticData.push([entry.pointer.writer, entry]);
        var rawStatic = new Uint8Array(this.serializeStatic());
        return corenodeClient.setStaticData(user, user.signMessage(rawStatic));
    }

    this.getFollowRequests = function() {
	var that = this;
        return corenodeClient.getFollowRequests(user.getPublicKeys()).then(function(reqs){
	    var all = reqs.map(decodeFollowRequest);
	    return getFollowerRoots().then(function(followerRoots) {
		var initialRequests = all.filter(function(freq){
		    if (followerRoots[freq.entry.owner] != null) {
			// delete our folder if they didn't reciprocate
			var ourDirForThem = followerRoots[freq.entry.owner];
			var ourKeyForThem = ourDirForThem.getKey().key;
			var keyFromResponse = freq.key.key;
			if (keyFromResponse == null || !arraysEqual(keyFromResponse, ourKeyForThem))
			    ourDirForThem.remove(that);
			else // add new entry to tree root
			    that.downloadEntryPoints([freq.entry]).then(function(treenode) {
				that.getAncestorsAndAddToTree(treenode, that);
			    });
			// add entry point to static data
			that.addToStaticData(freq.entry).then(function(res) {
			    return corenodeClient.removeFollowRequest(that.user.getPublicKeys(), that.user.signMessage(freq.rawCipher));
			});
			return false;
		    }
		    return followerRoots[freq.entry.owner] == null;
		});
		return Promise.resolve(initialRequests);
	    });
	});
    };

    var decodeFollowRequest = function(raw) {
        var pBoxKey = new Uint8Array(32);
        for (var i=0; i < 32; i++)
            pBoxKey[i] = raw[i]; // signing key is not used
        var tmp = new UserPublicKey(null, pBoxKey);
        var buf = new ByteArrayInputStream(raw);
        buf.read(32);
        var cipher = new Uint8Array(buf.read(raw.length - 32));
	var plaintext = user.decryptMessage(cipher, tmp);
	var input = new ByteArrayInputStream(plaintext);
	var rawEntry = input.readArray();
	var rawKey = input.readArray();
        return new FollowRequest(rawEntry.length > 0 ? EntryPoint.deserialize(rawEntry) : null, 
				 rawKey.length > 0 ? new SymmetricKey(rawKey) : null, raw);
    }
    
    this.uploadFragment = function(f, targetUser) {
        return dhtClient.put(f.getData(), targetUser.getPublicKeys());
    }

    this.uploadFragments = function(fragments, owner, sharer, mapKey, setProgressPercentage) {
	// now upload fragments to DHT
        var futures = [];
        for (var i=0; i < fragments.length; i++){
            if(setProgressPercentage != null){
                if(uploadFragmentTotal != 0){
                    var percentage = parseInt(++uploadFragmentCounter / uploadFragmentTotal * 100);
                    setProgressPercentage(percentage);
                    //document.title = "Peergos Uploading: " + percentage + "%" ;  
                }
            }
            futures[i] = this.uploadFragment(fragments[i], owner);
        }
        // wait for all fragments to upload
        return Promise.all(futures);
    }.bind(this);

    this.uploadChunk = function(metadata, owner, sharer, mapKey) {
        var buf = new ByteArrayOutputStream();
        metadata.serialize(buf);
        var metaBlob = buf.toByteArray();
	const btree = this.btree;
        console.log("Storing metadata blob of " + metaBlob.length + " bytes.");
	return this.dhtClient.put(metaBlob, owner.getPublicKeys()).then(function(blobHash){
	    return btree.put(sharer.getPublicKeys(), mapKey, blobHash);
	}).then(function(newBtreeRoot) {
	    var msg = newBtreeRoot;
	    var signed = sharer.signMessage(msg);
            return corenodeClient.addMetadataBlob(owner.getPublicKeys(), sharer.getPublicKeys(), signed)
		.then(function(added) {
		    if (!added) {
			console.log("Meta blob store failed.");
		return Promise.resolve(false);
		    }
		    return Promise.resolve(true);
		});
	});
    }

    this.getRoots = function() {
        const context = this;
        return corenodeClient.getStaticData(user).then(function(raw) {
            var buf = new ByteArrayInputStream(raw);
            var totalStaticLength = buf.readInt();
            var count = buf.readInt();
            var res = [];
            for (var i=0; i < count; i++) {
                var entry = EntryPoint.symmetricallyDecryptAndDeserialize(buf.readArray(), context.rootKey);
                res.push(entry);
		this.addToStaticData(entry);
            }
	    
            return this.downloadEntryPoints(res);
        }.bind(this));
    }.bind(this);

    this.downloadEntryPoints = function(entries) {
	// download the metadata blobs for these entry points
        var proms = [];
        for (var i=0; i < entries.length; i++)
	    proms[i] = this.btree.get(entries[i].pointer.writer.getPublicKeys(), entries[i].pointer.mapKey).then(function(hash) {
		return dhtClient.get(hash);
	    });

        return Promise.all(proms).then(function(result) {
            var entryPoints = [];
            for (var i=0; i < result.length; i++) {
                if (result[i].byteLength > 8) {
                    entryPoints.push([entries[i], FileAccess.deserialize(result[i])]);
                } else {
                    // these point to removed directories
		}
            }
            return Promise.resolve(entryPoints);
	});
    }

    this.getTreeRoot = function() {
	return Promise.resolve(this.rootNode);
    }
    
    this.getUserRoot = function() {
        return this.getTreeRoot().then(function(root) {
            return root.getChildren().then(function(children) {
                    if (children.length == 0)
                            throw "no children in user root!";
                    const userRoots = children.filter(function(e) {
                        return e.getFileProperties().name == this.username;
                    }.bind(this));
                    if (userRoots.length != 1)
                            throw  "user has "+ userRoots.length +" roots!";
                    return Promise.resolve(userRoots[0]);
            }.bind(this));
        }.bind(this));
    }.bind(this);

    this.getAncestorsAndAddToTree = function(treeNode, context) {
	try {
	    // don't need to add our own files this way, as we'll find them going down from our root
	    if (treeNode.getOwner() == context.username && !treeNode.isWritable())
		return Promise.resolve(true);
	    return treeNode.retrieveParent(context).then(function(parent) {
		if (parent == null)
		    return Promise.resolve(true);
		parent.addChild(treeNode);
		return context.getAncestorsAndAddToTree(parent, context);
	    });
	} catch (e) {
	    console.log(e);
	    return Promise.resolve(null);
	}
    }
    
    var createFileTree = function() {
	return this.getRoots().then(function(roots){
	    var entrypoints = roots.map(function(x) {return new FileTreeNode(new RetrievedFilePointer(x[0].pointer, x[1]), x[0].owner, x[0].readers, x[0].writers, x[0].pointer.writer);});
	    console.log("Entry points "+entrypoints);
	    var globalRoot = FileTreeNode.ROOT;
	    var proms = [];
	    
	    for (var i=0; i < entrypoints.length; i++) {
		var current = entrypoints[i];
		proms.push(this.getAncestorsAndAddToTree(current, this));
	    }
	    return Promise.all(proms).then(function(res) {
		return Promise.resolve(globalRoot);
	    });
	}.bind(this));
    }.bind(this);

// [SymmetricLocationLink], SymmetricKey
    this.retrieveAllMetadata = function(links, baseKey) {
        var proms = [];
        for (var i=0; i < links.length; i++) {
            var loc = links[i].targetLocation(baseKey);
            proms[i] = this.btree.get(loc.writer.getPublicKeys(), loc.mapKey).then(function(hash) {
		return dhtClient.get(hash);
	    });
        }
        return Promise.all(proms).then(function(rawBlobs) {
            var accesses = [];
            for (var i=0; i < rawBlobs.length; i++) {
                accesses[i] = [links[i].toReadableFilePointer(baseKey), rawBlobs[i].length > 0 ? FileAccess.deserialize(rawBlobs[i]) : null];
            }
	    const res = [];
	    for (var i=0; i < accesses.length; i++)
		if (accesses[i][1] != null)
		    res.push(accesses[i]);
            return Promise.resolve(res);
        });
    }

    this.getMetadata = function(loc) {
        return corenodeClient.getMetadataBlob(loc.owner, loc.writer, loc.mapKey).then(function(buf) {
            var unwrapped = new ByteArrayInputStream(buf).readArray();
            return FileAccess.deserialize(unwrapped);
        });
    }

    this.downloadFragments = function(hashes, setProgressPercentage) {
        var result = {}; 
        result.fragments = [];
        result.nError = 0;
        
        var proms = [];
        for (var i=0; i < hashes.length; i++)
            proms.push(dhtClient.get(hashes[i]).then(function(val) {
                result.fragments.push(val);
                //console.log("Got Fragment.");
                if(setProgressPercentage != null){
                    if(downloadFragmentTotal != 0){
                        var percentage = parseInt(++downloadFragmentCounter / downloadFragmentTotal * 100);
                        setProgressPercentage(percentage);
                        //document.title = "Peergos Downloading: " + percentage + "%" ;  
                    }
                }
            }).catch(function() {
                result.nError++;
            }));

        return Promise.all(proms).then(function (all) {
            console.log("All done.");<commit_msg>fixed downloading multichunk files.<commit_after>if (typeof module !== "undefined")
    var nacl = require("./nacl");
if (typeof module !== "undefined")
    var erasure = require("./erasure");

function humanSort(l,r) {
                            const comp = l.toLowerCase().localeCompare(r.toLowerCase());
                            return comp != 0 ? comp : l.localeCompare(r);
};
// API for the User interface to use
/////////////////////////////

/////////////////////////////
// UserPublicKey methods
function UserPublicKey(publicSignKey, publicBoxKey) {
    this.pSignKey = publicSignKey; // 32 bytes
    this.pBoxKey = publicBoxKey; // 32 bytes

    // ((err, publicKeyString) -> ())
    this.getPublicKeys = function() {
        var tmp = new Uint8Array(this.pSignKey.length + this.pBoxKey.length);
        tmp.set(this.pSignKey, 0);
        tmp.set(this.pBoxKey, this.pSignKey.length);
        return tmp;
    }
    
    // (Uint8Array, User, (nonce, cipher) -> ())
    this.encryptMessageFor = function(input, us) {
        var nonce = createNonce();
        return concat(nacl.box(input, nonce, this.pBoxKey, us.sBoxKey), nonce);
    }
    
    // Uint8Array => boolean
    this.unsignMessage = function(sig) {
        return nacl.sign.open(sig, this.pSignKey);
    }
}
//Uint8Array => UserPublicKey
UserPublicKey.fromPublicKeys = function(both) {
    var pSign = slice(both, 0, 32);
    var pBox = slice(both, 32, 64);
    return new UserPublicKey(pSign, pBox);
}

UserPublicKey.HASH_BYTES = 34;
// Uint8Array => Uint8Array
UserPublicKey.hash = function(arr) {
    const hash = sha256(arr);
    const multihash = new Uint8Array(hash.length+2);
    multihash[0] = 0x12;
    multihash[1] = 0x20;
    for(var i=0; i < hash.length; i++) multihash[i+2] = hash[i];

    return multihash;
}

function createNonce(){
    return window.nacl.randomBytes(24);
}

/////////////////////////////
// User methods
// (string, string, (User -> ())
function generateKeyPairs(username, password, cb) {
    var hash = sha256(nacl.util.decodeUTF8(password));
    var salt = nacl.util.decodeUTF8(username)
    
    return new Promise(function(resolve, reject) {
        scrypt(hash, salt, 17, 8, 96, 1000, function(keyBytes) {
            var bothBytes = nacl.util.decodeBase64(keyBytes);
            var signBytes = bothBytes.subarray(0, 32);
            var boxBytes = bothBytes.subarray(32, 64);
	    var rootKeyBytes = bothBytes.subarray(64, 96);
            resolve({
		user:new User(nacl.sign.keyPair.fromSeed(signBytes), nacl.box.keyPair.fromSecretKey(new Uint8Array(boxBytes))),
		root:new SymmetricKey(new Uint8Array(rootKeyBytes))
	    });
        }, 'base64');
    });
}

function User(signKeyPair, boxKeyPair) {
    UserPublicKey.call(this, signKeyPair.publicKey, boxKeyPair.publicKey);
    this.sSignKey = signKeyPair.secretKey; // 64 bytes
    this.sBoxKey = boxKeyPair.secretKey; // 32 bytes
    
    // (Uint8Array, (nonce, sig) => ())
    this.hashAndSignMessage = function(input, cb) {
        signMessage(this.hash(input), cb);
    }
    
    // (Uint8Array => Uint8array)
    this.signMessage = function(input) {
        return nacl.sign(input, this.sSignKey);
    }
    
    // (Uint8Array, (err, literals) -> ())
    this.decryptMessage = function(cipher, them) {
        var nonce = slice(cipher, cipher.length-24, cipher.length);
        cipher = slice(cipher, 0, cipher.length-24);
        return nacl.box.open(cipher, nonce, them.pBoxKey, this.sBoxKey);
    }

    this.getSecretKeys = function() {
        var tmp = new Uint8Array(this.sSignKey.length + this.sBoxKey.length);
        tmp.set(this.sSignKey, 0);
        tmp.set(this.sBoxKey, this.sSignKey.length);
        return tmp;
    }
}

User.fromEncodedKeys = function(publicKeys, secretKeys) {
    return new User(toKeyPair(slice(publicKeys, 0, 32), slice(secretKeys, 0, 64)), toKeyPair(slice(publicKeys, 32, 64), slice(secretKeys, 64, 96)));
}

User.fromSecretKeys = function(secretKeys) {
    var publicBoxKey = new Uint8Array(32);
    nacl.lowlevel.crypto_scalarmult_base(publicBoxKey, slice(secretKeys, 64, 96))
    return User.fromEncodedKeys(concat(slice(secretKeys, 32, 64), 
                publicBoxKey),
                secretKeys);
}

User.random = function() {
    var secretBoxKey = window.nacl.randomBytes(32);
    var signSeed = window.nacl.randomBytes(32);
    return new User(nacl.sign.keyPair.fromSeed(signSeed), nacl.box.keyPair.fromSecretKey(new Uint8Array(secretBoxKey)));
}

function toKeyPair(pub, sec) {
    return {publicKey:pub, secretKey:sec};
}

/////////////////////////////
// SymmetricKey methods

function SymmetricKey(key) {
    if (key.length != nacl.secretbox.keyLength)
	throw "Invalid symmetric key: "+key;
    this.key = key;

    // (Uint8Array, Uint8Array[24]) => Uint8Array
    this.encrypt = function(data, nonce) {
    return nacl.secretbox(data, nonce, this.key);
    }

    // (Uint8Array, Uint8Array) => Uint8Array
    this.decrypt = function(cipher, nonce) {
    return nacl.secretbox.open(cipher, nonce, this.key);
    }

    // () => Uint8Array
    this.createNonce = function() {
    return nacl.randomBytes(24);
}
}
SymmetricKey.NONCE_BYTES = 24;
SymmetricKey.random = function() {
    return new SymmetricKey(nacl.randomBytes(32));
}

function FileProperties(name, size, modified, attr) {
    this.name = name;
    this.size = size;
    this.modified = modified;
    this.attr = attr; //  | HIDDEN

    this.serialize = function() {
        var buf = new ByteArrayOutputStream();
        buf.writeString(name);
        buf.writeDouble(size);
	buf.writeDouble(modified);
	buf.writeByte(attr);
        return buf.toByteArray();
    }

    this.getSize = function() {
        return size;
    }

    this.getModified = function() {
	return new Date(modified);
    }

    this.isHidden = function() {
	return this.attr & 1 != 0;
    }
}

FileProperties.deserialize = function(raw) {
    const buf = new ByteArrayInputStream(raw);
    var name = buf.readString();
    var size = buf.readDouble();
    var modified = buf.readDouble();
    var attr = buf.readByte();
    return new FileProperties(name, size, modified, attr);
}

function asyncErasureEncode(original, originalBlobs, allowedFailures) {
    var worker = new Worker("scripts/erasure.js");
    var prom = new Promise(function(resolve, reject){
	worker.onmessage = function(e) {
	    var bfrags = e.data;
	    resolve(bfrags);
	};
	worker.postMessage({original:original, originalBlobs:EncryptedChunk.ERASURE_ORIGINAL, allowedFailures:EncryptedChunk.ERASURE_ALLOWED_FAILURES});
    });
    return prom;
}

function Fragment(data) {
    this.data = data;

    this.getData = function() {
        return data;
    }
}
Fragment.SIZE = 128*1024;

function EncryptedChunk(encrypted) {
    this.auth = slice(encrypted, 0, window.nacl.secretbox.overheadLength);
    this.cipher = slice(encrypted, window.nacl.secretbox.overheadLength, encrypted.length);

    this.generateFragments = function() {
        return asyncErasureEncode(this.cipher, EncryptedChunk.ERASURE_ORIGINAL, EncryptedChunk.ERASURE_ALLOWED_FAILURES).then(function(bfrags) {
            var frags = [];
            for (var i=0; i < bfrags.length; i++)
		frags[i] = new Fragment(bfrags[i]);
	    return Promise.resolve(frags);
	});
    }

    this.getAuth = function() {
        return this.auth;
    }

    this.decrypt = function(key, nonce) {
        return key.decrypt(concat(this.auth, this.cipher), nonce);
    }
}
EncryptedChunk.ERASURE_ORIGINAL = 40;
EncryptedChunk.ERASURE_ALLOWED_FAILURES = 10;

function Chunk(data, key) {
    this.data = data;
    this.key = key;
    this.nonce = window.nacl.randomBytes(SymmetricKey.NONCE_BYTES);
    this.mapKey = window.nacl.randomBytes(32);

    this.encrypt = function() {
        return new EncryptedChunk(key.encrypt(data, this.nonce));
    }
}
Chunk.MAX_SIZE = Fragment.SIZE*EncryptedChunk.ERASURE_ORIGINAL
// string, File, SymmetricKey, Location, SymmetricKey -> 
function FileUploader(name, file, key, parentLocation, parentparentKey, setProgressPercentage) {
    this.props = new FileProperties(name, file.size, Date.now(), 0);
    if (key == null) key = SymmetricKey.random();

    // Process and upload chunk by chunk to avoid running out of RAM, in reverse order to build linked list
    this.nchunks = Math.ceil(file.size/Chunk.MAX_SIZE);
    
    this.file = file;
    this.key = key;
    this.parentLocation = parentLocation;
    this.parentparentKey = parentparentKey;
    this.setProgressPercentage = setProgressPercentage;

    this.uploadChunk = function(context, owner, writer, chunkIndex, file, nextLocation) {

	var that = this;
	return new Promise(function(resolve, reject) {
	    console.log("uploading chunk: "+chunkIndex + " of "+file.name);
	    var filereader = new FileReader();
	    filereader.file_name = file.name;
	    filereader.onload = function(){
		const data = new Uint8Array(this.result);
		var chunk = new Chunk(data, key)
		const encryptedChunk = chunk.encrypt();
		encryptedChunk.generateFragments().then(function(fragments){
                    console.log("Uploading chunk with %d fragments\n", fragments.length);
		    context.uploadFragments(fragments, owner, writer, chunk.mapKey, setProgressPercentage).then(function(hashes){
			const retriever = new EncryptedChunkRetriever(chunk.nonce, encryptedChunk.getAuth(), hashes, nextLocation);
			const metaBlob = FileAccess.create(chunk.key, that.props, retriever, parentLocation, parentparentKey);
			context.uploadChunk(metaBlob, owner, writer, chunk.mapKey).then(function() {
			    resolve(new Location(owner, writer, chunk.mapKey));
			});
		    });
		});
	    }
	    filereader.readAsArrayBuffer(file.slice(chunkIndex*Chunk.MAX_SIZE, Math.min((1+chunkIndex)*Chunk.MAX_SIZE, file.size)));
	}).then(function(nextL) {
	    if (chunkIndex > 0)
		return that.uploadChunk(context, owner, writer, chunkIndex-1, file, nextL, setProgressPercentage);
	    return Promise.resolve(nextL);
	});
    }.bind(this);
    
    this.upload = function(context, owner, writer) {
	return this.uploadChunk(context, owner, writer, this.nchunks-1, this.file, null);
    }.bind(this);
}

/////////////////////////////
// Util methods

// byte[] input and return
function encryptBytesToBytes(input, pubKey) {
    return Java.to(encryptUint8ToUint8(input, pubKey), "byte[]");
}

// byte[] cipher and return
function decryptBytesToBytes(cipher, privKey) {
    return Java.to(decryptUint8ToUint8(cipher, privKey), "byte[]");
}

function uint8ArrayToByteArray(arr) {
    return Java.to(arr, "byte[]");
}

function slice(arr, start, end) {
    if (end < start)
        throw "negative slice size! "+start + " -> " + end;
    var r = new Uint8Array(end-start);
    for (var i=start; i < end; i++)
        r[i-start] = arr[i];
    return r;
}

function concat(a, b, c) {
    if (a instanceof Array) {
        var size = 0;
        for (var i=0; i < a.length; i++)
            size += a[i].length;
        var r = new Uint8Array(size);
        var index = 0;
        for (var i=0; i < a.length; i++)
            for (var j=0; j < a[i].length; j++)
                r[index++] = a[i][j];
        return r;
    }
    var r = new Uint8Array(a.length+b.length+(c != null ? c.length : 0));
    for (var i=0; i < a.length; i++)
        r[i] = a[i];
    for (var i=0; i < b.length; i++)
        r[a.length+i] = b[i];
    if (c != null)
        for (var i=0; i < c.length; i++)
            r[a.length+b.length+i] = c[i];
    return r;
}

function arraysEqual(a, b) {
    if (a.length != b.length)
        return false;

    for (var i=0; i < a.length; i++)
        if (a[i] != b[i])
            return false;
    return true;
}

function get(path, onSuccess, onError) {

    var request = new XMLHttpRequest();
    request.open("GET", path);

    request.onreadystatechange=function()
    {
        if (request.readyState != 4)
            return;

        if (request.status == 200) 
            onSuccess(request.response);
        else
            onError(request.status);
    }

    request.send();
}

function getProm(url) {
    return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.responseType = 'arraybuffer';

    req.onload = function() {
        // This is called even on 404 etc
        // so check the status
        if (req.status == 200) {
        resolve(new Uint8Array(req.response));
        }
        else {
        reject(Error(req.statusText));
        }
    };

    req.onerror = function() {
        reject(Error("Network Error"));
    };

    req.send();
    });
}

function post(path, data, onSuccess, onError) {

    var request = new XMLHttpRequest();
    request.open("POST" , path);
    request.responseType = 'arraybuffer';

    request.onreadystatechange=function()
    {
        if (request.readyState != 4)
            return;

        if (request.status == 200) 
            onSuccess(request.response);
        else
            onError(request.status);
    }

    request.send(data);
}

function postProm(url, data) {
    return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('POST', window.location.origin + "/" + url);
    req.responseType = 'arraybuffer';

    req.onload = function() {
        // This is called even on 404 etc
        // so check the status
        if (req.status == 200) {
        resolve(new Uint8Array(req.response));
        }
        else {
        reject(Error(req.statusText));
        }
    };

    req.onerror = function() {
        reject(Error("Network Error"));
    };

    req.send(data);
    });
}

//Java is Big-endian
function readInt(bytes, position) {
    var count = 0;
    for(var i = position; i <  position +4 ; i++)
        count = count << 8 + bytes[i];
    return count;
}

//Java is Big-endian
function writeInt(bytes, position, intValue) {
    intValue |= 0;
    for(var i = position + 3; position <= i ; i--)
        bytes[position] = intValue & 0xff;
        intValue >>= 8;
}

function DHTClient() {
    //
    //put
    //
    this.put = function(valueData, owner) {        
        var arrays = [valueData, owner];
        var buffer = new ByteArrayOutputStream();
        buffer.writeInt(0); // PUT Message
        for (var iArray=0; iArray < arrays.length; iArray++) 
            buffer.writeArray(arrays[iArray]);
        return postProm("dht/put", buffer.toByteArray()).then(function(resBuf){
            var stream = new ByteArrayInputStream(resBuf);
	    var res = stream.readInt();
            if (res == 1) {
		var key = stream.readArray();
		return Promise.resolve(key);
	    }
            return Promise.reject("DHT put failed");
        });
    };
    //
    //get
    //
    this.get = function(keyData) { 
        var buffer = new ByteArrayOutputStream();
        buffer.writeInt(1); // GET Message
        buffer.writeArray(keyData);
        return postProm("dht/get", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("Fragment download failed");
        });
    };
}

function CoreNodeClient() {
    //String -> fn- >fn -> void
    this.getPublicKey = function(username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getPublicKey", buffer.toByteArray()).then(function(raw) {
	    var arr = new ByteArrayInputStream(raw).readArray();
	    var res = arr.length = 0 ? null : UserPublicKey.fromPublicKeys(arr);
	    return Promise.resolve(res);
	});
    };
    
    //UserPublicKey -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.setStaticData = function(owner, signedStaticData) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(signedStaticData);
        return postProm("core/setStaticData", buffer.toByteArray()); 
    };
    
    //String -> fn- >fn -> void
    this.getStaticData = function(owner) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        return postProm("core/getStaticData", buffer.toByteArray());
    };
    
    //Uint8Array -> fn -> fn -> void
    this.getUsername = function(publicKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(publicKey);
        return postProm("core/getUsername", buffer.toByteArray()).then(function(res) {
            const name = new ByteArrayInputStream(res).readString();
            return Promise.resolve(name);
        });
    };
    
    // () -> List<String>
    this.fetchUsernames = function() {
        return getProm("core/getAllUsernamesGzip").then(function(res) {
            const din = new ByteArrayInputStream(res);
	    var res = [];
	    for (;;) {
		var uname = din.readString();
		if (uname == "")
		    break;
		res.push(uname);
	    }
            return Promise.resolve(res);
        });
    };    
    
    //String -> Uint8Array -> Uint8Array -> fn -> fn ->  boolean 
    this.addUsername = function(username, encodedUserKey, signed, staticData) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedUserKey);
        buffer.writeArray(signed);
        buffer.writeArray(staticData);
        return postProm("core/addUsername", buffer.toByteArray()).then(
	    function(res){
		return Promise.resolve(res[0]);
	    });
    };
    
    //Uint8Array -> Uint8Array -> fn -> fn -> void
    this.followRequest = function( target,  encryptedPermission) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(target);
        buffer.writeArray(encryptedPermission);
        return postProm("core/followRequest", buffer.toByteArray()).then(function(res){
	    return Promise.resolve(res[0]==1);
	});
    };
    
    //String -> Uint8Array -> fn -> fn -> void
    this.getFollowRequests = function( user) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(user);
        return postProm("core/getFollowRequests", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var size = buf.readInt();
            var n = buf.readInt();
            var arr = [];
            for (var i=0; i < n; i++) {
                var t = buf.readArray();
                arr.push(t);
            }
            return Promise.resolve(arr);
        });
    };
    
    //Uint8Array -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.removeFollowRequest = function( target,  signedRequest) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(target);
        buffer.writeArray(signedRequest);
        return postProm("core/removeFollowRequest", buffer.toByteArray()).then(function(res) {
            return Promise.resolve(new ByteArrayInputStream(res).readByte() == 1);
        });
    };

    //String -> Uint8Array -> Uint8Array -> fn -> fn -> void
    this.banSharingKey = function(username,  encodedSharingPublicKey,  signedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedSharingPublicKey);
        buffer.writeArray(signedHash); 
        return postProm("core/banSharingKey", buffer.toByteArray());
    };

    //Uint8Array -> Uint8Array -> Uint8Array -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.addMetadataBlob = function( owner,  encodedSharingPublicKey, sharingKeySignedPayload) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner);
        buffer.writeArray(encodedSharingPublicKey);
        buffer.writeArray(sharingKeySignedPayload);
        return postProm("core/addMetadataBlob", buffer.toByteArray()).then(function(res) {
            return Promise.resolve(new ByteArrayInputStream(res).readByte() == 1);
        });
    };
    
    //String -> Uint8Array -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.removeMetadataBlob = function( owner,  writer,  mapKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(writer.getPublicKeys());
        buffer.writeArray(writer.signMessage(mapKey));
        return postProm("core/removeMetadataBlob", buffer.toByteArray());
    };

    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.removeUsername = function( username,  userKey,  signedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(userKey);
        buffer.writeArray(signedHash);
        return post("core/removeUsername", buffer.toByteArray());
    };

    //String -> fn -> fn -> void
    this.getSharingKeys = function( username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getSharingKeys", buffer.toByteArray());
    };
    
    //writing key -> btree root hash
    this.getMetadataBlob = function(encodedSharingKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(encodedSharingKey.getPublicKeys());
        return postProm("core/getMetadataBlob", buffer.toByteArray());
    };
    
    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.isFragmentAllowed = function( owner,  encodedSharingKey,  mapkey,  hash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(owner);
        buffer.writeArray(encodedSharingKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(hash);
        return postProm("core/isFragmentAllowed", buffer.toByteArray());
    };
    
    //String -> fn -> fn -> void
    this.getQuota = function(user) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(user.getPublicKeys());
        return postProm("core/getQuota", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var quota = buf.readInt() << 32;
            quota += buf.readInt();
            return Promise.resolve(quota);
        });
    };
    
    //String -> fn -> fn -> void
    this.getUsage = function(username) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        return postProm("core/getUsage", buffer.toByteArray());
    };
    
    //String  -> Uint8Array  -> Uint8Array -> fn -> fn -> void
    this.getFragmentHashes = function( username, sharingKey, mapKey) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("core/getFragmentHashes", buffer.toByteArray());
    };

    //String  -> Uint8Array  -> Uint8Array -> Uint8Array -> [Uint8Array] -> Uint8Array -> fn -> fn -> void
    this.addFragmentHashes = function(username, encodedSharingPublicKey, mapKey, metadataBlob, allHashes, sharingKeySignedHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeString(username);
        buffer.writeArray(encodedShaaringPublicKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(metadataBlob);

        buffer.writeInt(allHashes.length);
        for (var iHash=0; iHash  <  allHashes.length; iHash++) 
            buffer.writeArray(allHashes[iHash]);

        buffer.writeArray(sharingKeySignedHash);
        
        return postProm("core/addFragmentHashes", buffer.toByteArray());
    };

    
    this.registerFragmentStorage = function(spaceDonor, address, port, owner, signedKeyPlusHash) {
        var buffer = new ByteArrayOutputStream();
        buffer.writeArray(spaceDonor.getPublicKeys());
        buffer.writeArray(address);
        buffer.writeInt(port);
        buffer.writeArray(owner.getPublicKeys());
        buffer.writeArray(signedKeyPlusHash);
        return postProm("core/registerFragmentStorage", buffer.toByteArray());
    };
};

function BTree() {
    
    this.put = function(sharingKey, mapKey, value) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(0); // PUT
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        buffer.writeArray(value);
        return postProm("btree/put", buffer.toByteArray()).then(function(resBuf){
            var stream = new ByteArrayInputStream(resBuf);
	    var res = stream.readInt();
            if (res == 1) {
		var key = stream.readArray();
		return Promise.resolve(key);
	    }
            return Promise.reject("BTree put failed");
        });
    }

    this.get = function(sharingKey, mapKey) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(1); // GET
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("btree/get", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("BTree get failed");
        });
    }

    this.remove = function(sharingKey, mapKey) {
	var buffer = new ByteArrayOutputStream();
	buffer.writeInt(2); // DELETE
        buffer.writeArray(sharingKey);
        buffer.writeArray(mapKey);
        return postProm("btree/delete", buffer.toByteArray()).then(function(res) {
            var buf = new ByteArrayInputStream(res);
            var success = buf.readInt();
            if (success == 1)
                return Promise.resolve(buf.readArray());
            return Promise.reject("BTree delete failed");
        });
    }
}

function UserContext(username, user, rootKey, dhtClient,  corenodeClient) {
    this.username  = username;
    this.user = user;
    this.rootKey = rootKey;
    this.dhtClient = dhtClient;
    this.corenodeClient = corenodeClient;
    this.btree = new BTree();
    this.staticData = []; // array of map entry pairs
    this.rootNode = null;
    this.sharingFolder = null;
    this.usernames = [];

    this.init = function() {
	FileTreeNode.ROOT.clear();
	this.staticData = [];
	var context = this;
	return createFileTree().then(function (rootNode) {
	    context.rootNode = rootNode;
	    return rootNode.getChildren(context).then(function (children) {
		for (var i=0; i < children.length; i++) {
		    if (children[i].getFileProperties().name == context.username)
			return children[i].getChildren().then(function(ourdirs) {
			    for (var j=0; j < ourdirs.length; j++) {
				if (ourdirs[j].getFileProperties().name == "shared") {
				    context.sharingFolder = ourdirs[j];
				    return context.corenodeClient.fetchUsernames().then(function(unames){
					context.usernames = unames;
					return Promise.resolve(true);
				    });
				}
			    }
			    return Promise.reject("Couldn't find shared folder!");
			});
		}
		return Promise.reject("No root directory found!");
	    });
	});
    }.bind(this);

    this.getUsernames= function() {
            return this.usernames;
    }

    this.isRegistered = function() {
        return corenodeClient.getUsername(user.getPublicKeys()).then(function(res){
            return Promise.resolve(username == res);
        });
    }

    this.serializeStatic = function() {
        var buf = new ByteArrayOutputStream();
        buf.writeInt(this.staticData.length);
        for (var i = 0; i < this.staticData.length; i++)
            buf.writeArray(this.staticData[i][1].serializeAndSymmetricallyEncrypt(this.rootKey));
        return buf.toByteArray();
    }

    this.register = function() {
        console.log("registering "+username);
        var rawStatic = this.serializeStatic();
        var signed = user.signMessage(concat(nacl.util.decodeUTF8(username), user.getPublicKeys(), rawStatic));
        return corenodeClient.addUsername(username, user.getPublicKeys(), signed, rawStatic);
    }

    this.createEntryDirectory = function(directoryName) {
        var writer = User.random();
        var rootMapKey = window.nacl.randomBytes(32); // root will be stored under this in the core node
        var rootRKey = SymmetricKey.random();

        // and authorise the writer key
        const rootPointer = new ReadableFilePointer(this.user, writer, rootMapKey, rootRKey);
        const entry = new EntryPoint(rootPointer, this.username, [], []);
        
        return this.addToStaticData(entry).then(function(res) {
            var root = DirAccess.create(rootRKey, new FileProperties(directoryName, 0, Date.now(), 0));
            return this.uploadChunk(root, this.user, writer, rootMapKey).then(function(res) {
		if (res)
		    return Promise.resolve(new RetrievedFilePointer(rootPointer, root));
		return Promise.reject();
	    });
        }.bind(this));
    }

    var getSharingFolder = function() {
	return this.sharingFolder;
    }.bind(this);
    
    var getFriendRoots = function() {
	var ourname = this.username;
	return this.rootNode.getChildren(this).then(function (children) {
	    return Promise.resolve(children.filter(function(froot){return froot.getOwner() != ourname}));
	}.bind(this));
    }.bind(this);

    var getFollowers = function() {
	return getSharingFolder().getChildren(this).then(function(friendFolders){
	    return Promise.resolve(
                        friendFolders.map(function(froot){
                                return froot.getFileProperties().name;
                        }).sort(humanSort));
	});
    }.bind(this);

    var getFollowing = function() {
	var that = this;
	return getFriendRoots().then(function(friendRoots) {
	    return Promise.resolve(friendRoots.map(function(froot){return froot.getOwner()}).filter(function(name){return name != that.username;}));
	});
    }.bind(this)

    var getFollowerRoots = function() {
	return getSharingFolder().getChildren(this).then(function(friendFolders){
	    var res = {};
	    friendFolders.map(function(froot){return res[froot.getFileProperties().name] = froot;});
	    return Promise.resolve(res);
	});
    }.bind(this);

    this.getSocialState = function() {
	return this.getFollowRequests().then(function(pending) {
	    return getFollowerRoots().then(function(followerRoots) {
		return getFriendRoots().then(function(followingRoots) {
		    return Promise.resolve(new SocialState(pending, followerRoots, followingRoots));
		});
	    });
	});
    }.bind(this);
    
    this.sendInitialFollowRequest = function(targetUsername) {
	return this.sendFollowRequest(targetUsername, SymmetricKey.random());
    }

    // FollowRequest, boolean, boolean
    this.sendReplyFollowRequest = function(initialRequest, accept, reciprocate) {
	var context = this;
	var theirUsername = initialRequest.entry.owner;
	// if accept, create directory to share with them, note in entry points (they follow us)
	if (!accept) {
	    // send a null entry and null key (full rejection)
	    //TODO
	    return Promise.resolve(true);
	}
	return getSharingFolder().mkdir(theirUsername, context, initialRequest.key).then(function(friendRoot) {
	    // add a note to our static data so we know who we sent the read access to
	    const entry = new EntryPoint(friendRoot.readOnly(), context.username, [theirUsername], []);
            const targetUser = initialRequest.entry.pointer.owner;
	    return context.addToStaticData(entry).then(function(res) {
		// create a tmp keypair whose public key we can prepend to the request without leaking information
                var tmp = User.random();
		var buf = new ByteArrayOutputStream();
		buf.writeArray(entry.serialize());
		if (! reciprocate) {
		    buf.writeArray(new Uint8Array(0)); // tell them we're not reciprocating
		    var plaintext = buf.toByteArray();
                    var payload = targetUser.encryptMessageFor(plaintext, tmp);

                    return corenodeClient.followRequest(initialRequest.entry.pointer.owner.getPublicKeys(), concat(tmp.pBoxKey, payload));
		}
		// if reciprocate, add entry point to their shared dirctory (we follow them) and them 
		buf.writeArray(initialRequest.entry.pointer.baseKey.key); // tell them we are reciprocating
		var plaintext = buf.toByteArray();
                var payload = targetUser.encryptMessageFor(plaintext, tmp);
		
                return corenodeClient.followRequest(initialRequest.entry.pointer.owner.getPublicKeys(), concat(tmp.pBoxKey, payload)).then(function(res) {
		    return context.addToStaticData(initialRequest.entry);
		});
	    }).then(function(res) {
		// remove original request
		return corenodeClient.removeFollowRequest(context.user.getPublicKeys(), context.user.signMessage(initialRequest.rawCipher));
	    });
	});
    }

    // string, RetrievedFilePointer, SymmetricKey
    this.sendFollowRequest = function(targetUsername, requestedKey) {
	var sharing = getSharingFolder();
	var that = this;
	return sharing.getChildren(this).then(function(children) {
	    var alreadyFollowed = false;
	    for (var i=0; i < children.length; i++)
		if (children[i].getFileProperties().name == targetUsername)
		    alreadyFollowed = true;
	    if (alreadyFollowed)
		return Promise.resolve(false);
	    // check for them not reciprocating
	    return getFollowing().then(function(following) {
		var alreadyFollowed = false;
		for (var i=0; i < following.length; i++)
		    if (following[i] == targetUsername)
			alreadyFollowed = true;
		if (alreadyFollowed)
		    return Promise.resolve(false);

		return that.corenodeClient.getPublicKey(targetUsername).then(function(targetUser) {
		    return sharing.mkdir(targetUsername, that).then(function(friendRoot) {
			
			// add a note to our static data so we know who we sent the read access to
			const entry = new EntryPoint(friendRoot.readOnly(), that.username, [targetUsername], []);
			return that.addToStaticData(entry).then(function(res) {
			    // send details to allow friend to follow us, and optionally let us follow them		
			    // create a tmp keypair whose public key we can prepend to the request without leaking information
			    var tmp = User.random();
			    var buf = new ByteArrayOutputStream();
			    buf.writeArray(entry.serialize());
			    buf.writeArray(requestedKey != null ? requestedKey.key : new Uint8Array(0));
			    var plaintext = buf.toByteArray();
			    var payload = targetUser.encryptMessageFor(plaintext, tmp);
			    
			    return corenodeClient.followRequest(targetUser.getPublicKeys(), concat(tmp.pBoxKey, payload));
			});
		    });
		});
            });
	});
    }.bind(this);

    this.sendWriteAccess = function(targetUser) {
        // create sharing keypair and give it write access
        var sharing = User.random();
        var rootMapKey = window.nacl.randomBytes(32);

        // add a note to our static data so we know who we sent the private key to
        var friendRoot = new ReadableFilePointer(user, sharing, rootMapKey, SymmetricKey.random());
        return this.corenodeClient.getUsername(targetUser.getPublicKeys()).then(function(name) {
            const entry = new EntryPoint(friendRoot, this.username, [], [name]);
            return this.addToStaticData(entry).then(function(res) {
                // create a tmp keypair whose public key we can append to the request without leaking information
                var tmp = User.random();
                var payload = entry.serializeAndEncrypt(tmp, targetUser);
                return corenodeClient.followRequest(targetUser.getPublicKeys(), concat(tmp.pBoxKey, payload));
            });
        }.bind(this));
    }.bind(this);

    this.addToStaticData = function(entry) {
	for (var i=0; i < this.staticData.length; i++)
	    if (this.staticData[i][1].equals(entry))
		return Promise.resolve(true);
        this.staticData.push([entry.pointer.writer, entry]);
        var rawStatic = new Uint8Array(this.serializeStatic());
        return corenodeClient.setStaticData(user, user.signMessage(rawStatic));
    }

    this.getFollowRequests = function() {
	var that = this;
        return corenodeClient.getFollowRequests(user.getPublicKeys()).then(function(reqs){
	    var all = reqs.map(decodeFollowRequest);
	    return getFollowerRoots().then(function(followerRoots) {
		var initialRequests = all.filter(function(freq){
		    if (followerRoots[freq.entry.owner] != null) {
			// delete our folder if they didn't reciprocate
			var ourDirForThem = followerRoots[freq.entry.owner];
			var ourKeyForThem = ourDirForThem.getKey().key;
			var keyFromResponse = freq.key.key;
			if (keyFromResponse == null || !arraysEqual(keyFromResponse, ourKeyForThem))
			    ourDirForThem.remove(that);
			else // add new entry to tree root
			    that.downloadEntryPoints([freq.entry]).then(function(treenode) {
				that.getAncestorsAndAddToTree(treenode, that);
			    });
			// add entry point to static data
			that.addToStaticData(freq.entry).then(function(res) {
			    return corenodeClient.removeFollowRequest(that.user.getPublicKeys(), that.user.signMessage(freq.rawCipher));
			});
			return false;
		    }
		    return followerRoots[freq.entry.owner] == null;
		});
		return Promise.resolve(initialRequests);
	    });
	});
    };

    var decodeFollowRequest = function(raw) {
        var pBoxKey = new Uint8Array(32);
        for (var i=0; i < 32; i++)
            pBoxKey[i] = raw[i]; // signing key is not used
        var tmp = new UserPublicKey(null, pBoxKey);
        var buf = new ByteArrayInputStream(raw);
        buf.read(32);
        var cipher = new Uint8Array(buf.read(raw.length - 32));
	var plaintext = user.decryptMessage(cipher, tmp);
	var input = new ByteArrayInputStream(plaintext);
	var rawEntry = input.readArray();
	var rawKey = input.readArray();
        return new FollowRequest(rawEntry.length > 0 ? EntryPoint.deserialize(rawEntry) : null, 
				 rawKey.length > 0 ? new SymmetricKey(rawKey) : null, raw);
    }
    
    this.uploadFragment = function(f, targetUser) {
        return dhtClient.put(f.getData(), targetUser.getPublicKeys());
    }

    this.uploadFragments = function(fragments, owner, sharer, mapKey, setProgressPercentage) {
	// now upload fragments to DHT
        var futures = [];
        for (var i=0; i < fragments.length; i++){
            if(setProgressPercentage != null){
                if(uploadFragmentTotal != 0){
                    var percentage = parseInt(++uploadFragmentCounter / uploadFragmentTotal * 100);
                    setProgressPercentage(percentage);
                    //document.title = "Peergos Uploading: " + percentage + "%" ;  
                }
            }
            futures[i] = this.uploadFragment(fragments[i], owner);
        }
        // wait for all fragments to upload
        return Promise.all(futures);
    }.bind(this);

    this.uploadChunk = function(metadata, owner, sharer, mapKey) {
        var buf = new ByteArrayOutputStream();
        metadata.serialize(buf);
        var metaBlob = buf.toByteArray();
	const btree = this.btree;
        console.log("Storing metadata blob of " + metaBlob.length + " bytes.");
	return this.dhtClient.put(metaBlob, owner.getPublicKeys()).then(function(blobHash){
	    return btree.put(sharer.getPublicKeys(), mapKey, blobHash);
	}).then(function(newBtreeRoot) {
	    var msg = newBtreeRoot;
	    var signed = sharer.signMessage(msg);
            return corenodeClient.addMetadataBlob(owner.getPublicKeys(), sharer.getPublicKeys(), signed)
		.then(function(added) {
		    if (!added) {
			console.log("Meta blob store failed.");
		return Promise.resolve(false);
		    }
		    return Promise.resolve(true);
		});
	});
    }

    this.getRoots = function() {
        const context = this;
        return corenodeClient.getStaticData(user).then(function(raw) {
            var buf = new ByteArrayInputStream(raw);
            var totalStaticLength = buf.readInt();
            var count = buf.readInt();
            var res = [];
            for (var i=0; i < count; i++) {
                var entry = EntryPoint.symmetricallyDecryptAndDeserialize(buf.readArray(), context.rootKey);
                res.push(entry);
		this.addToStaticData(entry);
            }
	    
            return this.downloadEntryPoints(res);
        }.bind(this));
    }.bind(this);

    this.downloadEntryPoints = function(entries) {
	// download the metadata blobs for these entry points
        var proms = [];
        for (var i=0; i < entries.length; i++)
	    proms[i] = this.btree.get(entries[i].pointer.writer.getPublicKeys(), entries[i].pointer.mapKey).then(function(hash) {
		return dhtClient.get(hash);
	    });

        return Promise.all(proms).then(function(result) {
            var entryPoints = [];
            for (var i=0; i < result.length; i++) {
                if (result[i].byteLength > 8) {
                    entryPoints.push([entries[i], FileAccess.deserialize(result[i])]);
                } else {
                    // these point to removed directories
		}
            }
            return Promise.resolve(entryPoints);
	});
    }

    this.getTreeRoot = function() {
	return Promise.resolve(this.rootNode);
    }
    
    this.getUserRoot = function() {
        return this.getTreeRoot().then(function(root) {
            return root.getChildren().then(function(children) {
                    if (children.length == 0)
                            throw "no children in user root!";
                    const userRoots = children.filter(function(e) {
                        return e.getFileProperties().name == this.username;
                    }.bind(this));
                    if (userRoots.length != 1)
                            throw  "user has "+ userRoots.length +" roots!";
                    return Promise.resolve(userRoots[0]);
            }.bind(this));
        }.bind(this));
    }.bind(this);

    this.getAncestorsAndAddToTree = function(treeNode, context) {
	try {
	    // don't need to add our own files this way, as we'll find them going down from our root
	    if (treeNode.getOwner() == context.username && !treeNode.isWritable())
		return Promise.resolve(true);
	    return treeNode.retrieveParent(context).then(function(parent) {
		if (parent == null)
		    return Promise.resolve(true);
		parent.addChild(treeNode);
		return context.getAncestorsAndAddToTree(parent, context);
	    });
	} catch (e) {
	    console.log(e);
	    return Promise.resolve(null);
	}
    }
    
    var createFileTree = function() {
	return this.getRoots().then(function(roots){
	    var entrypoints = roots.map(function(x) {return new FileTreeNode(new RetrievedFilePointer(x[0].pointer, x[1]), x[0].owner, x[0].readers, x[0].writers, x[0].pointer.writer);});
	    console.log("Entry points "+entrypoints);
	    var globalRoot = FileTreeNode.ROOT;
	    var proms = [];
	    
	    for (var i=0; i < entrypoints.length; i++) {
		var current = entrypoints[i];
		proms.push(this.getAncestorsAndAddToTree(current, this));
	    }
	    return Promise.all(proms).then(function(res) {
		return Promise.resolve(globalRoot);
	    });
	}.bind(this));
    }.bind(this);

// [SymmetricLocationLink], SymmetricKey
    this.retrieveAllMetadata = function(links, baseKey) {
        var proms = [];
        for (var i=0; i < links.length; i++) {
            var loc = links[i].targetLocation(baseKey);
            proms[i] = this.btree.get(loc.writer.getPublicKeys(), loc.mapKey).then(function(hash) {
		return dhtClient.get(hash);
	    });
        }
        return Promise.all(proms).then(function(rawBlobs) {
            var accesses = [];
            for (var i=0; i < rawBlobs.length; i++) {
                accesses[i] = [links[i].toReadableFilePointer(baseKey), rawBlobs[i].length > 0 ? FileAccess.deserialize(rawBlobs[i]) : null];
            }
	    const res = [];
	    for (var i=0; i < accesses.length; i++)
		if (accesses[i][1] != null)
		    res.push(accesses[i]);
            return Promise.resolve(res);
        });
    }

    this.getMetadata = function(loc) {
	return this.btree.get(loc.writer.getPublicKeys(), loc.mapKey).then(function(blobHash) {
	    return this.dhtClient.get(blobHash).then(function(raw) {
		return Promise.resolve(FileAccess.deserialize(raw));
	    });
	}.bind(this));
    }.bind(this);

    this.downloadFragments = function(hashes, setProgressPercentage) {
        var result = {}; 
        result.fragments = [];
        result.nError = 0;
        
        var proms = [];
        for (var i=0; i < hashes.length; i++)
            proms.push(dhtClient.get(hashes[i]).then(function(val) {
                result.fragments.push(val);
                //console.log("Got Fragment.");
                if(setProgressPercentage != null){
                    if(downloadFragmentTotal != 0){
                        var percentage = parseInt(++downloadFragmentCounter / downloadFragmentTotal * 100);
                        setProgressPercentage(percentage);
                        //document.title = "Peergos Downloading: " + percentage + "%" ;  
                    }
                }
            }).catch(function() {
                result.nError++;
            }));

        return Promise.all(proms).then(function (all) {
            console.log("All done.");
{'fraction_non_alphanumeric': 0.13378534810649909, 'fraction_numerical': 0.006989691858221707, 'mean_word_length': 2.7455506462402446, 'pattern_counts': {'":': 0, '<': 78, '<?xml version=': 0, '>': 218, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 21, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4694036', 'n_tokens_mistral': 28048, 'n_tokens_neox': 26607, 'n_words': 5696}