JavaScriptでFormDataを使わずにマルチパートでPostをする

はじめに
====

Android 2.3などの古いブラウザではFormDataでバイナリファイルをPost出来ない。
そこで、マルチパートの構造を自前で作成して、Postすることが必要になる。
Cordovaなどのハイブリッドアプリでの運用を想定。

マルチパートデータ構造
====

マルチパートの構造についての解説
http://d.hatena.ne.jp/satox/20110726/1311665904

### ヘッダ部
コンテントタイプに以下を指定します。

    Content-Type: multipart/form-data; boundary=「バウンダリ文字列」\r\n

### ボディ部
  テキストデータ

    --「バウンダリ文字列」\r\n
    Content-Disposition: form-data; name="「フォームデータ名」"\r\n
    \r\n
    フォームデータの本体\r\n

　バイナリデータ

    Content-Disposition: form-data; name="「フォームデータ名」"; filename="ファイル名"\r\n
    Content-Type: application/octet-stream\r\n
    Content-Transfer-Encoding: binary\r\n
    \r\n
    バイナリデータの本体

`Content-Type`は、アップロードする内容によって適切なものにします。また、`Content-Transfer-Encoding`は`binary`以外(例えば`base64`)にしても無視されて、バイナリとして扱われます。



### フッタ部
マルチパートのフォームデータの最後に必要

    --「バウンダリ文字列」--\r\n


サンプルコード
====
 `index.html`と同じ階層にある`sample.jpg`ファイルをajaxにより読み込み、それを`http://サーバー名/uploader.php`にマルチパートでPostする。`Form`要素の`name`としては`userfile`、アップロード時の仮のファイル名は`myimage.png`として固定。

    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <link rel="stylesheet" href="css/style.css">
        <script>
            function sendPost() {
                
                var oReq = new XMLHttpRequest();
                oReq.open("GET","sample.jpg", true);
                oReq.responseType = "arraybuffer";
                oReq.onload = function(oEvent) {
                    var arrayBuffer = oReq.response;
                    console.log( "len = " +  arrayBuffer.byteLength );
                    
                    var request = new XMLHttpRequest();
                    request.open("POST",'http://サーバー名/uploader.php',true);
                    var boundary = createBoundary();
                    request.setRequestHeader( "Content-Type", 'multipart/form-data; boundary=' + boundary );
                    
                    var buffer = unicode2buffer( 
                        '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="userfile"; filename="myimage.png"\r\n'
                                                 + 'Content-Type: image/png\r\n\r\n'
                    );
                    
                    var buffer = appendBuffer( buffer , 
                        arrayBuffer
                    );
                    
                    var buffer = appendBuffer( buffer , 
                        unicode2buffer(
                            '\r\n' + '--' + boundary + '--'
                        )
                    );
                    
                    request.send( buffer );
                    alert("send!");
                }
                oReq.send(null);
                
            }
    
            function createBoundary() {
                var multipartChars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
                var length = 30 + Math.floor( Math.random() * 10 );
                var boundary = "---------------------------";
                for (var i=0;i < length; i++) {
                    boundary += multipartChars.charAt( Math.floor( Math.random() * multipartChars.length ) );
                }
                return boundary;
            }
    
            function unicode2buffer(str){
            
                var n = str.length,
                    idx = -1,
                    byteLength = 512,
                    bytes = new Uint8Array(byteLength),
                    i, c, _bytes;
            
                for(i = 0; i < n; ++i){
                    c = str.charCodeAt(i);
                    if(c <= 0x7F){
                        bytes[++idx] = c;
                    } else if(c <= 0x7FF){
                        bytes[++idx] = 0xC0 | (c >>> 6);
                        bytes[++idx] = 0x80 | (c & 0x3F);
                    } else if(c <= 0xFFFF){
                        bytes[++idx] = 0xE0 | (c >>> 12);
                        bytes[++idx] = 0x80 | ((c >>> 6) & 0x3F);
                        bytes[++idx] = 0x80 | (c & 0x3F);
                    } else {
                        bytes[++idx] = 0xF0 | (c >>> 18);
                        bytes[++idx] = 0x80 | ((c >>> 12) & 0x3F);
                        bytes[++idx] = 0x80 | ((c >>> 6) & 0x3F);
                        bytes[++idx] = 0x80 | (c & 0x3F);
                    }
                    if(byteLength - idx <= 4){
                        _bytes = bytes;
                        byteLength *= 2;
                        bytes = new Uint8Array(byteLength);
                        bytes.set(_bytes);
                    }
                }
                idx++;
                
                var result = new Uint8Array(idx);
                result.set(bytes.subarray(0,idx),0);
                
                return result.buffer;
            }
            
            function appendBuffer(buf1,buf2) {
                var uint8array = new Uint8Array(buf1.byteLength + buf2.byteLength);
                uint8array.set(new Uint8Array(buf1),0);
                uint8array.set(new Uint8Array(buf2),buf1.byteLength);
                return uint8array.buffer;
            }
        </script>
    </head>
    <body>
      <br />
        This is a template for Monaca app.
        <div onClick="sendPost();">Click!</div>
        <br>
        <div onClick="console.log(createBoundary());">Generate Boundary</div>
    </body>
    </html>

### ポイント

　画像データなどは、文字列化出来ないので、バイナリで扱うしかないことに注意。
　その場合、ヘッダ部やフッタ部などもひっくるめて全体をバイナリ化してあげなくてはいけない。

　なお、機種にもよるが、Android機でそもそもArrayBufferやUint8Arrayが使えなかったり、Ajaxでバイト列を扱えない場合、上記の機能も使えないので注意。