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); });
記 大坂 哲司