2015年10月29日
2016年3月16日
ブラウザーから画像などのファイルをアップロードします
ブラウザーからファイルをアップロードするためには、次のようなHTMLを記述します。2行目で”ファイルを選択”のボタンが表示され、ボタンを押すと、ファイルを選択するエクスプローラーが表示され、アップするファイルが選択できます。”送信”ボタンを押すと、action="cgi-bin/upload.cgi"にあるURLにデータを送信し、サーバー側で、それらのデータを受信、処理し、ファイルのアップロードが終わります。随分前、Javaで"multipart/form-data"のデータを処理した時、大変面倒であったことを思い出しました。最近になり、HTML5になってアップロードはどのようになった気になり、調べてみました。
<form action="cgi-bin/upload.cgi" method="post" enctype="multipart/form-data">
<input
type="file" name="datafile">
<input
type="submit" value="送信">
</form>
HTML5では、XMLHttpRequest通信、FormDataオブジェクトが利用でき、input文とJavascriptで簡単にアップロードができることが分かりました。FormDataオブジェクトによって、"multipart/form-data"を全く意識することはなくなりました。サーバー側(node.js)でもFormDataオブジェクトが処理できる”node-multiparty”があります。”node-multiparty”を利用すると、これまた簡単に受信したデータをサーバーローカルに保存してくれます。あまりに簡単に、一連のアップロード処理ができ、ただただ驚くのみです。
HTTP通信以外に、ソケット通信(socket.ioとsocket.io-stream)を利用したファイルアップロード機能も合わせて、盛り込んだサンプルを用意しました。ソケット通信が効率よくアップロードできると思ったのですが、あまり大差はありませんでした。使い勝手などを考えるとHTTP通信の方が便利だと思いました。
HTTP/2.0が登場すると、アップロードもバイナリーで行うことができるようになり、ここに挙げたコードも大きく変わるものと思います。
おまけ
HTTP通信によるアップロードで、JPEGファイルの場合、Exif情報をブラウザー内で削除する機能(piexif.jsを利用)を盛り込みました。スマホなどGPS搭載のデバイスでは緯度経度がJPEGファイル内に記録されています。複数の画像をアップロードすると、個人の位置情報や行動範囲が漏れてしまいます。個人情報保護のためにも、画像をアップするサイトでは、このような処理が搭載されていると、利用者は安心するのではないでしょうか。
usage
server
side
node
server.js
browser
side
http://localhost:8080/ (uploader.htmlが表示されます)
動作確認
Chrome46、IE11
・関連情報
・モジュール
"multiparty"
: "version": "4.1.2"
"socket.io"
: "version": "1.3.7"
"socket.io-stream"
: "version": "0.9.0"
"node.js"
: "v0.12.7"
・ブラウザーのHTML
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>uploader</title>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="js/socket.io-stream.js"></script>
<script type="text/javascript" src="piexif.js"></script>
</head>
<body>
Status: <span id="status">Waiting</span><br/>
Image file0(http): <input type="file" id="imageFile0" /><br/>
Image file1(socket): <input type="file" id="imageFile1" /><br/>
Image file2(stream): <input type="file" id="imageFile2" /><br/>
<pre id="imageReceivedMessage"></pre>
<img src="" id="imageSelected" style="width: 100px; height: 100px; border-width: 0px;"/><br/>
<hr/>
<script>
window.addEventListener('DOMContentLoaded', function() {
/*
JavaScriptでローカルファイルを自在に操る - File API
http://thinkit.co.jp/story/2013/02/06/3953
*/
document.querySelector("#imageFile0").addEventListener('change', function(e) {
if (window.File) {
var file = document.querySelector('#imageFile0').files[0];
console.log(file.name + " " + file.type);
var reader = new FileReader();
reader.onloadend = function (e) {
var raw = reader.result;
if (file.type.indexOf('image')==0) document.querySelector('#imageSelected').src = raw;
if (file.type.indexOf('image/jpeg')==0) {
var jpeg = piexif.remove(e.target.result);//Exif情報の削除
var str = atob(jpeg.split(",")[1]);
var data = [];
for (var p=0; p<str.length; p++) {
data[p] = str.charCodeAt(p);
}
var ua = new Uint8Array(data);
var blob = new Blob([ua], {name: file.name, type: file.type});
uploadFile('/upload', blob, file.name);
}
else
uploadFile('/upload', file);
}
reader.readAsDataURL(file);
}
}, true);
/*
Websockets with socket.io – simple image file upload
http://ckazbah.com/2014/03/26/websockets-with-socket-io-simple-image-file-upload/
*/
document.querySelector("#imageFile1").addEventListener('change', function(e) {
if (window.File) {
var file = e.target.files[0];
console.log(file.name);
var reader = new FileReader();
reader.onload = function(evt){
if (file.type.indexOf('image')==0)document.querySelector('#imageSelected').src = evt.target.result;
var jsonObject = {
'imageData': evt.target.result,
'imageName': file.name
}
// send a custom socket message to server
socket.emit('user image', jsonObject);
};
reader.readAsDataURL(file);
}
}, true);
/*
nkzawa/socket.io-stream
https://github.com/nkzawa/socket.io-stream
*/
document.querySelector("#imageFile2").addEventListener('change', function(e) {
if (window.File) {
var file = e.target.files[0];
console.log(file.name);
var stream = ss.createStream();
var blobStream = ss.createBlobReadStream(file);
/*
blobStream.on('data', function(chunk) {
console.log('data chunk.length:',chunk.length);
});
blobStream.on('end', function() {
console.log('end');
});
*/
ss(socket).emit('profile-image', stream, {name: file.name, size: file.size});
blobStream.pipe(stream);
}
}, true);
});
/*
XMLHttpRequest2 に関する新しいヒント
http://www.html5rocks.com/ja/tutorials/file/xhr2/
How to give a Blob uploaded as FormData a file name?
http://stackoverflow.com/questions/6664967/how-to-give-a-blob-uploaded-as-formdata-a-file-name
*/
function uploadFile(url, file, filename) {
var formData = new FormData();
if(filename){
formData.append("blob", file, filename);
}
else formData.append(file.name, file);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
// xhr.onload = function(e) { ... };
xhr.send(formData); // multipart/form-data
}
var Host = window.document.location.host;
Host = 'http://' + Host;
console.log("Host:"+Host);
var socket = io.connect(Host);
// image related socket
socket.on('user image', function(m){
document.querySelector('#imageReceivedMessage').innerHTML = "> "+m;
console.log("> "+m);
});
socket.on('connect', function(id){
document.querySelector('#status').innerHTML = 'Connected';
});
socket.on('setID', function(myID) {
console.log('receive id: ' + myID)
// $('#IDReceivedMessage').text(myID)
});
socket.on('message', function(m){
// $('#message').text("> "+m);
});
socket.on('disconnect', function(){
// $('#status').text('Disconnected');
});
</script>
</body>
</html>
・サーバープログラム
/*
アップロードされたファイルを受け取るサーバー
usage:
server side
node server.js
browser side
http://localhost:8080/ (uploader.htmlが表示される)
*/
var http = require('http');
var url = require('url');
var fs = require('fs');
var path = require('path');
var mime=require('mime');
var multiparty = require('multiparty'); // https://www.npmjs.com/package/multiparty
var socketio = require('socket.io');
var ss = require('socket.io-stream');
var port = 8080;
var uploadDirectory = './';
var server = http.createServer(function(req, res) {
console.log(req.headers);
var params = url.parse(req.url, true);
/*
Node.js Upload Handler
http://docs.ephox.com/display/tbio/Node.js+Upload+Handler
*/
if(params.pathname=="/upload"){
var form = new multiparty.Form();
form.parse(req);
form.on('file', function(name, file) {
var saveFilePath = uploadDirectory + file.originalFilename;
console.log(saveFilePath);
fs.rename(file.path, saveFilePath, function(err) {
if (err) {
// Handle problems with file saving
res.writeHead(500);
res.end();
} else {
// Respond to the successful upload with JSON.
// Use a location key to specify the path to the saved image resource.
// { location : '/your/uploaded/image/file'}
var textboxResponse = JSON.stringify({
location : saveFilePath
});
// If your script needs to receive cookies, set images.upload.credentials:true in
// the Textbox.io configuration and enable the following two headers.
// res.setHeader('Access-Control-Allow-Credentials', 'true');
// res.setHeader('P3P', 'CP="There is no P3P policy."');
res.statusCode = 200;
res.setHeader('Access-Control-Allow-Origin', 'true');
res.end(textboxResponse);
}
});
});
form.on('error', function(err) {
res.writeHead(500);
res.end();
});
return;
}
//要求ファイル(HTMLや画像など)の送信
else{
var filePath = url.parse(req.url).pathname;
if(filePath=="/")filePath="uploader.html";//デフォルトの設定
if(filePath=="/favicon.ico")return;
var fullPath = getRequestFilePath(filePath);
var ext = path.extname(fullPath);
console.log("filepath="+filePath+"\nfullPath="+fullPath+" ext="+ext+".");
fs.exists(fullPath, function(exists) {
if(exists){
var contentType = mime.lookup(ext);
console.log("ctype="+contentType);
if(contentType){
var statusCode = 200;
var body = fs.readFileSync(fullPath);
// Check HTTP Method
var method = req.method;
if (method == "GET" || method == "POST") {
res.setHeader("Pragma","no-cache");
res.setHeader("Expires","-1");
res.writeHead(statusCode, {
'Content-Type': contentType,
'Content-Length': body.length
});
res.write(body);
}
res.end();
}
}
});
}
}).listen(port);
// Get Request File Path
var getRequestFilePath = function (filePath) {
//console.log("filePath="+filePath);
var fullPath = path.join(__dirname,path.join(".\/", filePath));
return fullPath;
};
var io = socketio.listen(server);
// connection message/event is received
io.sockets.on('connection', function (client) {
console.log('Server started. Listening on http://localhost:' + port + '/')
var connected = true;
/*
nkzawa/socket.io-stream
https://github.com/nkzawa/socket.io-stream
*/
ss(client).on('profile-image', function(stream, data) {
var filename = path.basename(data.name);
stream.pipe(fs.createWriteStream(filename));
});
/*
Websockets with socket.io – simple image file upload
http://ckazbah.com/2014/03/26/websockets-with-socket-io-simple-image-file-upload/
*/
// image message received...yeah some refactoring is required but have fun with it...
client.on('user image', function (msg) {
var base64Data = decodeBase64Image(msg.imageData);
console.log(msg.imageName);
// write/save the image
// TODO: extract file's extension instead of hard coding it
fs.writeFile(msg.imageName, base64Data.data, function (err) {
if (err) {
console.log('ERROR:: ' + err);
throw err;
}
});
// I'm sending image back to client just to see and a way of confirmation. You can send whatever.
client.emit('user image', msg.imageName);
});
client.on('message', function (m) {
// do something
});
client.on('disconnect', function () {
connected = false;
});
//TODO: function to decode base64 to binary
function decodeBase64Image(dataString) {
var matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/),
response = {};
if (matches.length !== 3) {
return new Error('Invalid input string');
}
response.type = matches[1];
response.data = new Buffer(matches[2], 'base64');
return response;
}
});
記 大坂 哲司
おまけのおまけ(canvas画像をアップロードする)
上記の説明は、ユーザーローカルに保存されている画像などを、FileAPIを通して、サーバーにアップしました。一方で、デジカメ、スマホなどの撮影画像は高画質でサイズも大きいのが普通です。画像をアーカイブするサーバー以外ではこれほどの高画質を必要としません。そこでリサイズを行うため、画像を指定サイズのcanvasに展開し、canvas画像のアップロードする自分メモをここに記述します。
・ブラウザー側のスクリプト
//canvasの画像をhttp通信で、サーバーにアップロードする
function uploadImageHttp() {
var canvas = document.getElementById('canvas');
canvasData = canvas.toDataURL();
var base64Data = canvasData.split(',')[1], // Data URLからBase64のデータ部分のみを取得
data = window.atob(base64Data), // base64形式の文字列をデコード
buff = new ArrayBuffer(data.length),
arr = new Uint8Array(buff),
blob;
// blobの生成
for(var i = 0; i < data.length; i++){
arr[i] = data.charCodeAt(i);
}
var ua = new Uint8Array(arr);
blob = new Blob([ua], {name: filename + ".0.jpg", type: 'image/jpeg'});
uploadFile('/upload', blob, filename + ".0.jpg");
}
//canvasの画像をsocket通信で、サーバーにアップロードする
function uploadImageSocket() {
var canvas = document.getElementById('canvas');
/*
sending a canvas image over socket.io
http://stackoverflow.com/questions/24779288/sending-a-canvas-image-over-socket-io
Canvasで描画した画像を送信してサーバに保存する
http://qiita.com/0829/items/a8c98c8f53b2e821ac94
*/
// quality ranges from 0.00-1.00
var jpgQuality=0.90;
canvasData = canvas.toDataURL('image/jpeg', jpgQuality);// default PNG
var base64Data = canvasData.split(',')[1], // Data URLからBase64のデータ部分のみを取得
data = window.atob(base64Data), // base64形式の文字列をデコード
buff = new ArrayBuffer(data.length),
arr = new Uint8Array(buff),
blob;
// blobの生成
for(var i = 0; i < data.length; i++){
arr[i] = data.charCodeAt(i);
}
var ua = new Uint8Array(arr);
blob = new Blob([ua], {name: filename + ".1.jpg", type: 'image/jpeg'});
socket.emit('upload', {imageData: blob, imageName: filename + ".1.jpg"});
}
・サーバー側のスクリプト
client.on('upload', function (msg) {
fs.writeFile(msg.imageName, msg.imageData, function (err) {
if (err) {
console.log('ERROR:: ' + err);
throw err;
}
});
client.emit('user image', msg.imageName);
});
記 大坂 哲司