6

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??

Community
  • 1
  • 1
Yaakov5777
  • 309
  • 5
  • 15
  • “_How do I send a JSON object with an `ArrayBuffer`?_” sounds like [the XY problem](https://en.wikipedia.org/wiki/XY_problem), as the transmitted data doesn’t need to be in JSON. – Константин Ван Nov 26 '22 at 18:47

2 Answers2

5

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
Touffy
  • 6,309
  • 22
  • 28
  • Hi thanks for the answer, but can you give me some example code of how to create a new ArrayBuffer with the specific data? I'm not even sure how to modify an existing one – Yaakov5777 Mar 01 '19 at 10:18
  • so at the end of the day its actually stored in a Uint8Array variable, and not an actual arraybuffer? can you put in the code for sending this over a socket also, and especially in parts? That part I was really wondering about, how to send the file in parts, and also how are you getting the buffer data, with readAsArrayBuffer, or readAsBinaryText, or something else? And does this method slow down the sending time, if this is all done in real-time? – Yaakov5777 Mar 01 '19 at 11:27
  • 1. A Uint8Array is just a thin wrapper around an ArrayBuffer that allows you to read and write its values (you can't do that directly with an ArrayBuffer, see how I had to use a DataView). You cannot create a naked ArrayBuffer in the browser, @Yaakov5777. – Touffy Mar 01 '19 at 18:09
  • 1
    2. You should limit your question to one problem at a time. Honestly I think WebSocket is the wrong tool to send a file to a server (use a simple `fetch` with the File object as the body) but if you really want to go low level, you should split your question and have one for structured binary encoding and one for streaming over WebSockets. – Touffy Mar 01 '19 at 18:15
  • my question is one question: how to best upload a file with websockets. In order to do that, I'm attempting to use ArrayBuffer, since it seems the fastest way out of the 4 ways of file.readAs.... The question of splitting up the arraylist is only a detail of how to send the file in different parts using the arraylist, its part of the same question, I just need to send a file, in parts, using an arrraylist. How else do you think I should upload multiple files to a node.js server? How can I do it using fetch exactly, I'm not getting the file off a server, I'm uploading it from the client – Yaakov5777 Mar 03 '19 at 04:28
  • 1
    `fetch` allows you to make an HTTP POST (or PUT) request to your server with the file as its body. You don't need to parse the file as anything (even an ArrayBuffer) to do that, because the `fetch` API understands what a File object is, so the browser will natively stream the file over HTTP. – Touffy Mar 03 '19 at 18:29
  • unless you're streaming a lot of very tiny files, I think `fetch` is more efficient. And even then, over HTTP/2 it wouldn't be a problem to make lots of small requests. – Touffy Mar 03 '19 at 18:32
  • as to the multiple questions thing… ok, but this is StackOverflow. You are expected to try to solve the problem yourself and come back and ask more specific questions if you couldn't do it. In this case, while the title of your question is very (too) broad, your description makes it clear that you tried and had some specific issues, with which I tried to help. You did not describe any issue you had with splitting the file, so I must assume you haven't tried yet? – Touffy Mar 03 '19 at 18:46
  • @Touffy I do in fact planning on uploading many files with different sizes. Can fetch #1 upload in parts liike websocketcs can and #2 send multiple files? My question is "How to send JSON object with ArrayBuffer to websocket?" I still dont know how to do that at all – B''H Bi'ezras -- Boruch Hashem Mar 03 '19 at 20:33
  • @bluejayke Yes, it will stream the file, and yes if you send one request per file. You won't need to send metadata as JSON inside the payload because you'll have HTTP headers for that. – Touffy Mar 03 '19 at 20:43
  • So the only way to send json data along with an arrayBuffer is to encode the json into a binary format and concat the arrayBuffer? – Spankied Aug 18 '21 at 09:26
  • No, it's not the only way, but it is more efficient than base-64-encoding the buffer into the JSON and then including the JSON as a string. For example, MongoDB uses the BSON format which is a schema-based binary encoding of (restricted, but not quite as much as JSON) JS objects and naturally has buffers as a first-class data type. – Touffy Aug 19 '21 at 09:47
  • You can also use HTTP headers to send whatever metadata you would have sent in the JSON, and send the raw buffer as the body of the request/response (ideally with a correct Content-Type). Assuming that the JSON was indeed metadata and not too big. – Touffy Aug 19 '21 at 09:49
0

Serialize your Javascript objects that contain ArrayBuffers or Buffers into BSON and send them through the WebSocket.