javascriptarraysnode.jsjsonfileapi

How to send JSON object with ArrayBuffer to websocket?


I'm trying to simply upload a file to a node.js server.

To do this, I'm using the file API and readAsArrayBuffer. Here's the code that's called when the input file "change" event is fired, along with some hepler functions (and I'm using the COBY library for socket sending and other event setup, the binaryType is set to arraybuffer):

COBY.events = {
  "file": (e) => {
       files = Array.from(e.target.files);
       startReadingFile(files[0]);
   }
};

function startReadingFile(file) {
   readFileFrom(file, 0, chunkSize, (array, r) => {
       COBY.socketSend({"start uploading a file": {
           name:file.name,
           type:file.type,
           size:file.size,
           data:(array)
       }});
       console.log("didnt something?", r, Array.from(r));
   });
}

function readFileFrom(file, start, end, callback) {
   var sliced = file.slice(start, end);
   var reader = new FileReader();
   reader.onload = (event) => {
       result = (event.target.result);
       var arr = Array.from(new Uint8Array(result));
       if(callback && callback.constructor == Function) {
           currentPosition = end;
           callback(arr, result);
       }
   }
   reader.readAsArrayBuffer(sliced);
}

And on my server (I'm using the coby-node library which is the node.js version of the COBY client library):

var coby = require("coby-node");
var fs = require("fs");
var files = {};
var kilobyte = 1024;

function makeBigFile(name, number) {
    var test = fs.createWriteStream("./" + name, {flags: "w+"});
    console.log("OK?",name);
    [...Array(number)].forEach((x, i) => test.write(i+"\n"));
}

//makeBigFile("OKthere.txt", 12356);
coby.startAdanServer({
    onOpen:(cs) => {
        console.log("something just connected! Let's send it something");
     //   cs.send({"Whoa man !":1234});
        cs.send({asdf :3456789});
    },

    onAdanMessage: (cs, msg) => {
     //   console.log("HMM weird just got this message...", msg);
    },

    adanFunctions: {
        "do something important": (cs, data) => {
            console.log("I just got some message:", data);
            console.log(cs.server.broadcast);
            cs.server.broadcast({"look out":"here I am"}, {
                current: cs
            });

            cs.send({message:"OK I did it I think"});
        },
        "start uploading a file": (cs, data) => {
            if(data.data && data.data.constructor == Array) {
                var name = data["name"]
                files[name] = {
                    totalSize:data.size,
                    downloadedSize:0
                };
                
                files[name]["handler"] = fs.createWriteStream("./" + data.name, {
                    flags: "w+"
                });

                files[name]["handler"].on("error", (err) => {
                    console.log("OY vay", err);
                });
                cs.send({"ok dude I need more": {
                    name:name,
                    bytePositionToWriteTo:0,
                    totalLength:files[name]["totalSize"]
                }});
            }
        },
        "continue uploading file": (cs, data) => {
      
            var name = data.name;
            if(files[name]) {
                var handler = files[name]["handler"];

                var uint = Uint8Array.from(data.bufferArray);
                var myBuffer = Buffer.from(uint.buffer);
                var start = data.startPosition || 0,
                    end = myBuffer.byteLength + start;

                files[name].downloadedSize += myBuffer.byteLength;

                
                if(files[name].downloadedSize < files[name]["totalSize"]) {
                 
                    cs.send({"ok dude I need more": {
                        name:name,
                        bytePositionToWriteTo:files[name].downloadedSize,
                        totalLength:files[name]["totalSize"]
                    }});
                    try {
                        handler.write(myBuffer);
                    } catch(e) {
                        console.log("writing error: ", e);
                    }
                } else {
                    end = files[name]["totalSize"];
                    handler.write(myBuffer);
                    console.log("finished, I think?");
                    console.log(files[name].downloadedSize, "total: ", files[name]["totalSize"]);
                    console.log("   start: ", start, "end: ", end);
                }
                
                
            }
        }
    },
    intervalLength:1000
});

function startUnity() {
    coby.cmd(`./MyUnity/Editor/Unity.exe -batchmode -quit -projectPath "./MyUnity/totally empty" -executeMethod COBY.Start -logfile ./new123folder/wow.txt`, {
        onData:(data) => {
            console.log(data);
        },
        onError:(data) => {
            console.log(data);
        },
        onExit:(exitCode) => {
            console.log("exitted with code: " + exitCode);
        },
        onFail:(msg) => {
            console.log(msg);
        }
    });  
}

So far this actualy uploads a file, you can test it with npm install coby-node, but its taking a lot more time because I'm JSON.stringifing an Array.from(new Uint8Array(/* the ArrayBuffer result */)) and then on the server side I'm re-JSON parsing it, but how do I just send the actual ArrayBuffer to the websocket? I want to send the arraybuffer along with the name of the file and other data, so I want to include it in a JSON object, but when I JSON.stringify(/an ArrayBuffer/) the result is always [], and IDK how to send an ArrayBuffer with my own data ???

Also it seems to be taking a lot of time with Array.from(new Uint8Array(arrayBufer)) do you think readAsDataURL would be faster?

I AM able to, btw, send an arraybuffer by ITSELF via websocket with binayType="arraybuffer", but how do I include the filename with it??


Solution

  • So you want to send structured binary data. Most generic binary formats use a type-length-value encoding (ASN.1 or Nimn are good examples).

    In your case, you might want a simpler scheme because you have fixed fields: "name", "type", "size", "data". You already know their types. So you could got with just length-value. The idea is that each field in your byte stream begins with one or two bytes containing the length of the value. The parser will therefore know how many bytes to read before the next value, removing the need for delimiters.

    Let's say you want to encode this:

    {
      name: "file.txt",
      type: "text/plain",
      size: 4834,
      data: <an ArrayBuffer of length 4834>
    }
    

    The "size" field is actually going to be useful, because all other lengths fit in a single byte but the content length does not.

    So you make a new ArrayBuffer with the bytes:

    08 (length of the file name)
    66 69 6c 65 2e 74 78 74 (the string "file.txt")
    0a (length of the content type)
    74 65 78 74 2f 70 6c 61 69 6e (the string "text/plain")
    02 (you need two bytes to represent the size)
    12 e2 (the size, 4834 as an unsigned int16)
    ... and finally the bytes of the content
    

    To do that with client-side JavaScript is only slightly harder than with node.js Buffers. First, you need to compute the total length of the ArrayBuffer you'll need to send.

    // this gives you how many bytes are needed to represent the size
    let sizeLength = 1
    if (file.size > 0xffff)
      sizeLength = 4
    else if (file.size > 0xff)
      sizeLength = 2
    
    const utf8 = new TextEncoder()
    const nameBuffer = utf8.encode(file.name)
    const typeBuffer = utf8.encode(type)
    
    const length = file.size + sizeLength
      + nameBuffer.length + typeBuffer.length + 3
    
    const buffer = new Uint8Array(length)
    

    Now you just need to fill the buffer.

    Let's start with the lengths and copy the strings:

    let i = 0
    buffer[i] = nameBuffer.length
    buffer.set(i += 1, nameBuffer)
    buffer[i += nameBuffer.length] = typeBuffer.length
    buffer.set(i += 1, typeBuffer)
    buffer[i += typeBuffer.length] = sizeLength
    

    Then the file size must be written as the appropriate Int type:

    const sizeView = new DataView(buffer)
    sizeView[`setUInt${sizeLength*8}`](i += 1, file.size)
    

    Finally, copy the data:

    buffer.set(array, i + sizeLength) // array is your data