Less Talk, More Do Christopher Finke is a software engineer. He is available for birthday parties and bar mitzvahs.

Uploading form data and files with JavaScript (Mozilla)

January 30th, 2010

One problem I stumble across occasionally in writing Firefox extensions is properly uploading form data that includes a file - that is, assembling the POST request in JavaScript while still maintaining the sanctity of any file or string data. You can't just do this:

var request = "--boundary\r\n some text\r\n--boundary" + fileBytes + "\r\n--boundary--";

I had to spend a bit of time getting this just right in order to allow ScribeFire to upload media to Posterous, so I'm posting below the final solution at which I arrived; it was cobbled together from a dozen different examples I found around the Web (none of them solving the full problem), then lovingly massaged into the elegant function you see before you. With this function, you can pass in an array of fields and files, and the request will be crafted and returned to you, ready for upload.

Instructions for use are in the comment block at the top of the function.

function createPostRequest(args) {
  /**
   * Generates a POST request body for uploading.
   *
   * args is an associative array of the form fields.
   *
   * Example:
   * var args = { "field1": "abc", "field2" : "def", "fileField" :
   *              { "file": theFile, "headers" : [ "X-Fake-Header: foo" ] } };
   *
   * theFile is an nsILocalFile; the headers param for the file field is optional.
   *
   * This function returns an array like this:
   * { "requestBody" : uploadStream, "boundary" : BOUNDARY }
   *
   * To upload:
   *
   * var postRequest = createPostRequest(args);
   * var req = new XMLHttpRequest();
   * req.open("POST", ...);
   * req.setRequestHeader("Content-Type","multipart/form-data; boundary="+postRequest.boundary);
   * req.setRequestHeader("Content-Length", (postRequest.requestBody.available()));
   * req.send(postRequest.requestBody);
   */

  function stringToStream(str) {
    function encodeToUtf8(oStr) {
      var utfStr = oStr;
      var uConv = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                    .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
      uConv.charset = "UTF-8";
      utfStr = uConv.ConvertFromUnicode(oStr);

      return utfStr;
    }

    str = encodeToUtf8(str);

    var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
                   .createInstance(Components.interfaces.nsIStringInputStream);
    stream.setData(str, str.length);

    return stream;
  }

  function fileToStream(file) {
    var fpLocal  = Components.classes['@mozilla.org/file/local;1']
                     .createInstance(Components.interfaces.nsILocalFile);
    fpLocal.initWithFile(file);

    var finStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                      .createInstance(Components.interfaces.nsIFileInputStream);
    finStream.init(fpLocal, 1, 0, false);

    var bufStream = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
                      .createInstance(Components.interfaces.nsIBufferedInputStream);
    bufStream.init(finStream, 9000000);

    return bufStream;
  }

  var mimeSvc = Components.classes["@mozilla.org/mime;1"]
                  .getService(Components.interfaces.nsIMIMEService);
  const BOUNDARY = "---------------------------32191240128944"; 

  var streams = [];

  for (var i in args) {
    var buffer = "--" + BOUNDARY + "\r\n";
    buffer += "Content-Disposition: form-data; name=\"" + i + "\"";
    streams.push(stringToStream(buffer));

    if (typeof args[i] == "object") {
      buffer = "; filename=\"" + args[i].file.leafName + "\"";

      if ("headers" in args[i]) {
        if (args[i].headers.length > 0) {
          for (var q = 0; q < args[i].headers.length; q++){
            buffer += "\r\n" + args[i].headers[q];
          }
        }
      }

      var theMimeType = mimeSvc.getTypeFromFile(args[i].file);

      buffer += "\r\nContent-Type: " + theMimeType;
      buffer += "\r\n\r\n";

      streams.push(stringToStream(buffer));

      streams.push(fileToStream(args[i].file));
    }
    else {
      buffer = "\r\n\r\n";
      buffer += args[i];
      buffer += "\r\n";
      streams.push(stringToStream(buffer));
    }
  }

  var buffer = "--" + BOUNDARY + "--\r\n";
  streams.push(stringToStream(buffer));

  var uploadStream = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
                       .createInstance(Components.interfaces.nsIMultiplexInputStream);

  for (var i = 0; i < streams.length; i++) {
    uploadStream.appendStream(streams[i]);
  }

  return { "requestBody" : uploadStream, "boundary": BOUNDARY };
}

2010 Calendar - December

January 13th, 2010

12 - December

"December: Jesus is the reason for the season."

2010 Calendar - November

January 12th, 2010

11 - November

"November: It's probably cold out. Stay in and do a puzzle."

2010 Calendar - October

January 11th, 2010

10 - October

"October: Better harvest the crops before they freeze."

2010 Calendar - September

January 10th, 2010

09 - September

"September: Bonfires, ghost stories, and s'mores."

2010 Calendar - August

January 9th, 2010

08 - August

"August: Everyone loves to take a dip in the pool."

2010 Calendar - July

January 8th, 2010

07 - July

"July: School's out; video games are in."

2010 Calendar - June

January 7th, 2010

06 - June

"June: Perfect for getting a golden brown tan."

2010 Calendar - May

January 6th, 2010

05 - May

"May: That lawn isn't going to mow itself."

2010 Calendar - April

January 5th, 2010

04 - April

"April: Even wooden mannequins have to pay taxes."