20151029

2016316

ブラウザーから画像などのファイルをアップロードします

 

 ブラウザーからファイルをアップロードするためには、次のような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.iosocket.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が表示されます)

 

動作確認

              Chrome46IE11

 

リソースfileupload.zip

・関連情報

              HTML5で画像の読み書きをしてみよう

・モジュール

              "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);
	});

記 大坂 哲司