Filefoxでファイルの非同期アップロード with 複数パラメータ
1ヶ月前に書こうと思ってたネタなんですが,後回しにしていて今に至ります…
HTML5のFile APIを使って,って内容にするつもりだったんですが,W3CのHTML5の仕様にあるのはDrag and Dropだけで,ひょっとすると他の機能はFirefoxの独自機能じゃないかと思ってHTML5とは言わないことにします.
このネタを書こうと思ったのはこちらの記事でやってることが私にとって難しすぎたので,同じように感じた人の理解の助けになればと思ってです.
さてさて,FirefoxからサーバにAjaxでデータをアップロードする場合,画像などのバイナリデータを扱うことが多いかと思います.
しかし,XMLHttpRequestのsendはバイナリセーフじゃないので,次のようにFirefoxの独自機能であるsendAsBinaryで送ることになります.
せっかくなんで,HTML5の一機能であるドラッグアンドドロップで受け取った場合を示します.
function drop(e){
var xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php");
var dt = e.dataTransfer;
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
xhr.sendAsBinary(dt.files[0].getAsBinary());
}
e.dataTransferはドロップされたデータを保持していて,e.dataTransfer.filesにはそのデータのリストが格納されています.(※ファイルが1つでも配列です)
それをgetAsBinaryでバイナリデータを取り出し,sendAsBinaryでサーバに送っています.
ちなみに,Firefoxの独自機能を使わない場合は次のようになるはずです.(動作確認はしていません!)
function drop(e){
var xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php");
var dt = e.dataTransfer;
var reader = new FileReader();
reader.onload = function(e) {
xhr.send(encodeURIComponent(btoa(reader.result)));
};
reader.readAsBinaryString(files[0]);
}
ここで使われているのがいわゆるHTML5のFile APIです.
※PHPの場合,ファイルデータはfile_get_contents(“php://input”)で取得できるみたいです
このとき問題になるのは,ファイルデータ以外サーバに渡すことができないということです.
つまり,サーバ側で保存したい好きなファイル名をパラメータとして渡したりということができません.
それを解決するために複雑になっているのが先ほど挙げたこちらの記事です.
何をやっているのかというと,フォームを使ってファイルをアップロードする時に使われる multipart/form-data 形式でデータを送っています.
その multipart/form-data 形式というのはどういうものか?実際に見てみましょう.
次のようなフォームからファイルを選択してサーバにデータを渡します.
<form action="multipart.php" method="POST" enctype="multipart/form-data">
<input type="hidden" value="This is A" name="a">
<input type="hidden" value="これはBだよ" name="b">
<input type="file" name="file[]"><br>
<input type="submit" value="送信">
</form>
HTTPリクエストは次のようになります.(日本語とPNGデータは正しく表示されていません)
http://localhost/test/php/multipart.php
POST /test/php/multipart.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.9.2.6pre) Gecko/20100605 Ubuntu/10.04 (lucid) Namoroka/3.6.6pre FirePHP/0.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://localhost/test/php/multipart.php
Content-Type: multipart/form-data; boundary=---------------------------1300510839126694128622064124
Content-Length: 3480
-----------------------------1300510839126694128622064124
Content-Disposition: form-data; name="a"
This is A
-----------------------------1300510839126694128622064124
Content-Disposition: form-data; name="b"
〓“〓‚Œ〓〓B〓 〓‚ˆ
-----------------------------1300510839126694128622064124
Content-Disposition: form-data; name="file[]"; filename="hoge.png"
Content-Type: image/png
‰PNG
-----------------------------1300510839126694128622064124--
boundary=—————————1300510839126694128622064124
という記述がありますが,このboundaryは各データの境界を表すもので,データの中には絶対に現れない(はずの)文字列です.
multipart/form-data 形式の各データの最初は–boundaryで始まり,最後のデータの後には–boundary–が付与されます.
また,改行コードはCR+LFでないといけません.
ここまで理解できたらあとは簡単で,この multipart/form-data 形式のデータを自分で作成して,そのデータを送ればいいわけです.
次のソースではlocalhostのupload.phpに multipart/form-data 形式のデータを渡しています.
fileapi.js
function multipartFormat(data, boundary){
var stream = "--" + boundary;
for(var name in data) {
if(name == "file") {
var files = data[name];
for (var i = 0, len = files.length; i < len; i++){
stream += "\r\n";
stream += "Content-Disposition: form-data; name=\""
+ i + "\"; filename=\"" + files[i].name + "\"\r\n";
stream += "Content-Type: "+ files[i].type + "\r\n\r\n";
stream += files[i].getAsBinary() + "\r\n";
stream += "--" + boundary;
}
} else {
stream += "\r\n";
stream += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n";
stream += encodeURIComponent(data[name]) + "\r\n";
stream += "--" + boundary;
}
}
stream += "--";
return stream;
}
function dragenter(e){
e.stopPropagation();
e.preventDefault();
}
function dragover(e){
e.stopPropagation();
e.preventDefault();
}
function drop(e){
e.stopPropagation();
e.preventDefault();
var dt = e.dataTransfer;
var data = { a: "This is A", b: "これはBだよ", file: dt.files };
var boundary = "---------------------------17206603573748375831527334924";
var stream = multipartFormat(data, boundary);
var xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php");
xhr.setRequestHeader("Content-type", "multipart/form-data; boundary=" + boundary);
xhr.setRequestHeader("Content-length", stream.length);
xhr.sendAsBinary(stream);
xhr.onreadystatechange = function (evt) {
if (xhr.readyState == 4) {
if(xhr.status == 200) {
var data = eval("(" + xhr.responseText + ")");
console.log(data.status);
if(data.status == "success") {
alert("アップロードが完了しました.");
console.log(data.post.a + " / " + data.post.b);
} else {
alert("アップロードに失敗しました.");
}
} else {
alert("サーバにアクセスできません.");
}
}
}
}
function init() {
document.getElementById("droparea").addEventListener("dragenter", dragenter, false);
document.getElementById("droparea").addEventListener("dragover", dragover, false);
document.getElementById("droparea").addEventListener("drop", drop, false);
}
データを受け取るupload.phpは次のようになっています.
upload.php
<?php
require('FirePHPCore/fb.php');
fb(array_map(urldecode, $_POST));
foreach($_FILES as $file) {
if(is_uploaded_file($file["tmp_name"]))
$status = move_uploaded_file($file['tmp_name'], $file['name']) ? 'success' : 'error';
}
echo json_encode(array('status' => $status, 'post' => array_map(urldecode, $_POST)));
?>
ドラッグアンドドロップを試すページは以下のようなものです.
index.html
<html>
<head>
<title>File API Test</title>
<script type="text/javascript" src="fileapi.js"></script>
</head>
<body onLoad="init()">
<div id="droparea" style="width: 400px; height: 200px; border: solid 1px; text-align: center;">
ここにファイルをドラッグ&ドロップ
</div>
</body>
</html>
うん,ちゃんとファイルがアップロードできて,パラメータも渡すことができてますね.満足まんぞく.
参考
- FireFox3.6 のFileAPIを使ってドラッグ&ドロップでファイルアップロード, DoRuby!
- もっともっとFile APIを使ってサーバ側で受け取ってみる, @blog.justoneplanet.info
- multipart/form-data, suikawiki
- XMLHttpRequest()の使い方, とみぞーノート