Ajaxによるmultipart/postでの画像ファイルアップロード その3
こんにちは。内藤です。
前回
http://blog.asial.co.jp/1271
前々回
http://blog.asial.co.jp/1260
は、それぞれFormタグ、Cameraプラグインで画像を取得し、それをFormDataオブジェクトにBlobとして付与して、それをAjaxでサーバーにアップロードする方法について紹介しました。
今回は、FormDataではなく、手動でMultipart Postを生成してアップロードする方法について紹介します。FormDataは便利なのですが、Multipartの仕組みがすべてブラックボックス化されてしまっているため、内部の動作がよく分かりません。今回紹介する方法は、Multipartを自分で作成するため、他の言語で同様の機能を実装するのにも役立つかと思います。
かつては、JavaAppletなどでMultipart送信をするためによく使われた方法なのですが、最近ではあまり情報がないようなので、まとめてみました。
Multipartの基本的な仕組み
Multipartはもともと1つのメールに複数の添付ファイルを付与する方法を実現するために提唱されたもので、簡単にいうと、複数の添付ファイル(データ)を、boundaryと呼ばれる区切り文字で区切って、つなげたものをやり取りします。詳しくは
http://www.atmarkit.co.jp/ait/articles/0104/18/news002.html
などを確認してみて下さい。
WebのPostでもメールと同様に1回のPostに複数の添付ファイルを付与することが出来るようになっています。ただし、メールと異なる部分もあるので、注意が必要です。
http://www.ietf.org/rfc/rfc2616.txt
http://www.spencernetwork.org/reference/rfc2616-ja-HTTP1.1.txt
の19.4に、違いが記されています。特に、Content-Transfer-Encodingが利用出来ないことには注意です。このため、画像などは(base64ではなく)binaryとして送信する必要があります。
簡単なMultipartのサンプル
まずは、テキストのみでMultipartを実現する方法を確認してみます。
function sendPost() {
var request = new XMLHttpRequest();
request.open("POST",'http://your.server.url',true);
var boundary = createBoundary();
request.setRequestHeader( "Content-Type", 'multipart/form-data; boundary=' + boundary );
var body = '';
body += '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="myfield"\r\n\r\n';
body += "hogehoge";
body += '\r\n';
body += '--' + boundary + '--';
request.send( body );
alert("send!");
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;
}
<div onClick="sendPost();">Click!</div>
こんな感じになります。これは、HTMLでいうと、次のものと同じになります。
<form action="your.server.url" method="post" enctype="multipart/form-data">
<input type="text" name="myfield" />
<input type="submit" name="Send" />
</form>
boundary文字列は、適当に作ったものなので、文字種と長さが正しければ、なんでも構いません。
画像をUploadする場合
テキストと同じように、画像のUploadも出来るのですが、一つ大きな問題があります。
それは、画像がバイナリファイルであるため、テキストのように文字列オブジェクトで送ることが出来ないことです。
メールの場合は、base64などのエンコードを行って添付することも出来るのですが、Webページでは先に記述したようにContent-Transfer-Encodingに対応していないため出来ません。
そこで、ArrayBufferまたはUint8Array、Blobなどの形式で送ることが必要になります。これは、XHR2 (XmlHttpRequest 2)が必要となります。残念ながら、Android 2.3のストックブラウザはXHR2に対応していないので、この方法は利用出来ません。Android 2.3の場合、サーバー側でbase64エンコードしたファイルを扱うようにするか、もしくは、CordovaのFileTransferオブジェクトを利用して実現するのが良いと思います。
話を元に戻して、XHR2が使える前提で考えてゆきます。例えば、sample.jpgという画像(wwwの直下の画像)をアップロードする場合、sendPost関数は次のようになります。
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://your.server.url',true);
var boundary = createBoundary();
request.setRequestHeader( "Content-Type", 'multipart/form-data; boundary=' + boundary );
var buffer = unicode2buffer(
'--' + boundary + '\r\n' + 'Content-Disposition: forname="userfile"; filename="myimage.png"\r\n'
+ 'Content-Type: image/jpeg\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 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;
}
Stringではなく、ArrayBufferとしてContentを結合してゆきます。
なお、サーバー側のコードは、以前
http://blog.asial.co.jp/1260
と同じです。
Formのように画像を選択せずに、直接、ファイルをアップロードすることが出来ます。
Android 2.3への対応
先にも述べたように、XHR2に対応していないAndroid 2.3では、上記の方法は使えません。
http://blog.asial.co.jp/1313
では、Android 2.3用にArrayBufferやUint8Arrayクラスを作成しましたが、これは、あくまでCordovaを騙してArrayBufferやUint8Arrayがあるように見せかけるためのクラスなので、ブラウザの標準機能のみで実装する場合は、この方法は使えません。
そこで、ここでは簡単にFileTransferプラグインを利用する方法を示します。
上記と同様の処理を実現するためには、
function sendPost6() {
var oReq = new XMLHttpRequest();
oReq.open("GET","sample.jpg", false);
oReq.overrideMimeType('text/plain; charset=x-user-defined');
oReq.onload = function(oEvent) {
var response = oReq.responseText;
var length = response.length;
console.log( "length = " + length);
var array = new Array();
for (var i=0;i<length;i++) {
array.push( response.charCodeAt(i) & 0xff );
}
var uint8array = new Uint8Array( array );
var b64data = b64utils.encode( uint8array );
var imageUrl = "data:image/jpeg;base64," + b64data;
var options = new FileUploadOptions();
options.fileKey =