Google Drive API: Correct way to upload binary files via the Multipart API

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).

Answers:

Answer

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.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.