1

I'm trying to upload a binary file to Google Drive via the multipart upload API v3.

Here's the hex representation of the content of the file:

FF FE

For some reason the above content gets encoded as UTF-8 (I assume) when I try to POST it, enclosed in a multipart payload:

--BOUNDARY
Content-Type: application/json

{"name": "F.ini"}

--BOUNDARY
Content-Type: application/octet-stream

ÿþ                <-- in the outbound request, this gets UTF-8 encoded
--BOUNDARY--

Hex representation of the file that ultimately gets stored on server side:

C3 BF C3 BE

The problem only occurs in the sending stage: if I check the length of the content read from the file I always get 2; regardless of whether I use FileReader#readAsBinaryString or FileReader#readAsArrayBuffer (producing a string with length 2, and an ArrayBuffer with byteLength 2, respectively).

Here's the minimal code that I'm using to generate the multipart payload:

file = picker.files[0];    // 'picker' is a file picker
reader = new FileReader();
reader.onload = function (e) {
    content = e.target.result;
    boundary = "BOUNDARY";
    meta = '{"name": "' + file.name + '"}';
    console.log(content.length);    // gives 2 as expected

    payload = [
        "--" + boundary, "Content-Type: application/json", "", meta, "", "--" + boundary,
        "Content-Type: application/octet-stream", "", content, "--" + boundary + "--"
    ].join("\r\n");
    console.log(payload.length);    // say this gives n

    xhr = new XMLHttpRequest();
    xhr.open("POST", "/", false);
    xhr.setRequestHeader("Content-Type", "multipart/related; boundary=" + boundary);
    xhr.send(payload);              // this produces a request with a 'Content-Length: n+2' header
                                    // (corresponding to the length increase due to UTF-8 encoding)
};
reader.readAsBinaryString(file);

My question is twofold:

  • Is there a way to avoid this automatic UTF-8 encoding? (Probably not, because this answer implies that the UTF-8 encoding is part of the XHR spec.)
  • If not, what is the correct way to "inform" the Drive API that my file content is UTF-8 encoded? I have tried these approaches, with no success:
    • appending ; charset=utf-8 or ; charset=UTF-8 to the binary part's Content-Type header
    • doing the same to the HTTP header on the parent request (Content-Type: multipart/related; boundary=blablabla, charset=utf-8; also tried replacing the comma with a semicolon)

I need the multipart API because AFAIU the "simple" API does not allow me to upload into a folder (it only accepts a filename as metadata, via the Slug HTTP header, whereas the JSON metadata object in the multipart case allows a parent folder ID to be specified as well). (Just thought of mentioning this because the "simple" API handles things correctly when I directly POST the File (from the picker) or ArrayBuffer (from FileReader#readAsArrayBuffer) as the XHR's payload.)

I do not want to utilize any third-party libraries because

  • I want to keep things as light as possible, and
  • keeping aside reinventing-the-wheel and best-practices stuff, anything that is accomplished by a third party library should be doable via plain JS as well (this is just a fun exercise).

For the sake of completeness I tried uploading the same file via the GDrive web interface, and it got uploaded just fine; however the web interface seems to base64-encode the payload, which I would rather like to avoid (as it unnecessarily bloats up the payload, esp. for larger payloads which is my eventual goal).

Janaka Bandara
  • 1,024
  • 1
  • 12
  • 27
  • 1
    Can I ask you about your question? Although I'm not sure whether I could understand about your situation, how about using ``reader.readAsArrayBuffer(file)`` instead of ``reader.readAsBinaryString(file)``? By the way, from your script, it seems that there are no endpoint of Drive API and no headers including access token. The actual script has them? – Tanaike Aug 29 '18 at 22:52
  • `readAsArrayBuffer` and `readAsBinaryString` both give me the same output size (2); but since I want to compose a multipart payload I do need to read the content as a string. And regarding the code @Tanaike, you're correct about there being no Drive API endpoints/credentials; this is a minimal snippet that I wrote for uploading to a local server (http://localhost) for testing purposes, but the encoding behavior ("inflation"?) remains the same. – Janaka Bandara Aug 30 '18 at 01:11
  • Thank you for replying. From your reply, I posted an answer. Could you please confirm it? If this is not what you want, I'm sorry. – Tanaike Aug 30 '18 at 02:24
  • Did my answer show you the result what you want? Would you please tell me about it? That is also useful for me to study. If this works, other people who have the same issue with you can also base your question as a question which can be solved. If you have issues for my answer yet, feel free to tell me. I would like to study to solve your issues. – Tanaike Aug 31 '18 at 00:15
  • Sorry, I didn't get a chance to try this out until now; and it works perfectly! Marked your answer as accepted :) – Janaka Bandara Aug 31 '18 at 14:24
  • Thank you for replying. I'm glad your issue was solved. Thank you, too. – Tanaike Aug 31 '18 at 23:12

1 Answers1

2

How about this modification?

Modification points:

  • Used new FormData() for creating the multipart/form-data.
  • Used reader.readAsArrayBuffer(file) instead of reader.readAsBinaryString(file).
  • Send the file as a blob. In this case, the data is sent as application/octet-stream.

Modified script:

file = picker.files[0];    // 'picker' is a file picker
reader = new FileReader();
reader.onload = function (e) {
    var content = new Blob([file]);
    var meta = {name: file.name, mimeType: file.type};
    var accessToken = gapi.auth.getToken().access_token;
    var payload = new FormData();
    payload.append('metadata', new Blob([JSON.stringify(meta)], {type: 'application/json'}));
    payload.append('file', content);
    xhr = new XMLHttpRequest();
    xhr.open('post', 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart');
    xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
    xhr.onload = function() {
      console.log(xhr.response);
    };
    xhr.send(payload);
};
reader.readAsArrayBuffer(file);

Note:

  • In this modified script, I put the endpoint and the header including the access token. So please modify this for your environment.
  • In this case, I used a scope of https://www.googleapis.com/auth/drive.

Reference:

In my environment, I could confirmed that this script worked. But if this didn't work in your environment, I'm sorry.

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • Hey, sorry about the delay! And thanks, it works perfectly! I didn't try `FormData` previously because it results in a `multipart/form-data` content type whereas the API had specifically requested `multipart/related`; but apparently `multipart/form-data` works just fine! – Janaka Bandara Aug 31 '18 at 14:22
  • 1
    @Janaka Bandara Thank you for your additional information. Yes. ``multipart/form-data`` can also be used for this situation. So ``FormData`` makes users write easily the payload. – Tanaike Aug 31 '18 at 23:14