- виртмашина с Debian testing на борту, это будет сервер;
- браузер с поддержкой HTML5 File API (Google Chrome 19);
- файл гигов на 5 и более для тестирования.
Виртмашина с дебианчиком
nano .bashrc # для рута и своей учетки shopt -s histappend shopt -s cmdhist export PROMPT_COMMAND='history -a' export HISTFILESIZE=1000 export HISTSIZE=1000 export HISTCONTROL=ignoreboth:erasedups # репозитории nano /etc/apt/sources.list deb http://mirror.yandex.ru/debian/ testing main contrib non-free deb-src http://mirror.yandex.ru/debian/ testing main contrib non-free deb http://security.debian.org/ testing/updates main deb-src http://security.debian.org/ testing/updates main deb http://mirror.yandex.ru/debian/ testing-proposed-updates contrib non-free main deb-src http://mirror.yandex.ru/debian/ testing-proposed-updates contrib non-free main # доустановить пакеты aptitude update; aptitude safe-upgrade; aptitude full-upgrade aptitude install linux-headers-$(uname -r) dkms make gcc g++ aptitude install git-core curl build-essential openssl libssl-dev wget |
Node.js
# mkdir -p /root/build/node && cd $_ # git clone https://github.com/joyent/node.git . # git checkout v0.6.19 # ./configure --openssl-libpath=/usr/lib/ssl # make # make test # make install # node -v |
Socket.io
$ mkdir -p ~/node && cd $_ $ npm install socket.io # после заливки программ на сервер, запустить тут веб-сервер # node app.js |
Код программы
// на клиенте // по щелчку на кнопке «старт» в сокет уходит событие fileMeta с именем и размером файла; // на событие «чтение файла ридером» вешается обработчик, который отправляет в сокет событие fileData //с именем файла и куском считанных бинарных данных var socket = io.connect(); // connect at the same host / port as your website ... socket.on('nextChunk', onSocketNextChunk); socket.on('fileProcessed', onSocketFileProcessed); ... function onClickStartUpload() { ... fReader = new FileReader(); ... fReader.onload = function(evnt) { socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result}); } socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size}); … // на сервере, приняв событие fileMeta, код проверяет наличие файла и открывает его на запись; // открытие файла отправляет в сокет (в браузер) событие nextChunk // с номером следующего чанка и числом отработанных процентов var app = require('http').createServer(httpResponder) , io = require('socket.io').listen(app) ... app.listen(8080); // http://servername:8080/ io.sockets.on('connection', onSocketConnect); ... function onSocketConnect(socket) { socket.on( 'fileMeta', function(data) { onSocketFileMeta(data, socket); } ); socket.on( 'fileData', function(data) { onSocketFileData(data, socket); } ); } … function onSocketFileMeta (data, socket) { ... var stat = fs.statSync(fullName); if(stat.isFile()) { fObj.rcvdBytes = stat.size; ... fs.open(fullName, 'a', 0644, function(err, fd) { onFileOpen(err, fd, fName, socket); }); … function getNextChunk(fObj, socket) { var pct = (fObj.rcvdBytes / fObj.fSize) * 100; var chunkNum = fObj.rcvdBytes / chunkSize; socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct }); // на клиенте, обработчик события nextChunk считывает из файла следующий чанк данных, // после чего обработчик события «чтение файла ридером» отправляет в сокет событие fileData function onSocketNextChunk (data) { ... var blob = getNextBlob(selectedFile, nextByte, chunkSize); fReader.readAsBinaryString(blob); … function getNextBlob(fileObj, startPos, numBytes) { var endPos = startPos + Math.min( numBytes, (fileObj.size - startPos) ); if(fileObj.webkitSlice) return fileObj.webkitSlice(startPos, endPos); // на сервере, приняв событие fileData, код ветвится на три дорожки: // в приемном буфере еще есть место — запросить следующий чанк; // буфер полон — сбросить его на диск и запросить следующий чанк; // все данные уже здесь — завершить запись в файл, переместить его в репозиторий и отправить уведомление браузеру function onSocketFileData(data, socket) { ... if(fObj.rcvdBytes == fObj.fSize) { fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) { onFileWriteDone(err, written, buffer, fName, socket); }); } else if(fObj.bytesBuf.length > bufMaxSize) { fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) { onFileWriteBuffer(err, written, buffer, fName, socket); }); } else { getNextChunk(fObj, socket); } … function onFileWriteBuffer(err, written, buffer, fName, socket) { fObj.bytesBuf = ''; ... getNextChunk(fObj, socket); } … function onFileWriteDone(err, written, buffer, fName, socket) { ... fs.close(fObj.fHandle, function(err) { fObj.fHandle = ''; ... }); ... util.pump(inp, out, function() { fs.unlink(tempName, function () { socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'}); }); }); } … и так далее |
Запуск и тестирование
rsync -av "/home/valik/Desktop/node/" "node:node/"
$ cd node; mkdir Repo; mkdir Temp; node app.js
Полный код аплоадера
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <title>File Uploader</title> <script src="/socket.io/socket.io.js"></script> <script type="text/javascript" charset="utf-8"> window.addEventListener("load", onDocReady); var socket = io.connect(); // connect at the same host / port as your website var fReader = ''; // FileReader object var fileName = ''; // selected file name var selectedFile = ''; // file object var mbBytes = 1048576; // bytes in megabyte var chunkSize = 3 * mbBytes; // chunk size in bytes socket.on('nextChunk', onSocketNextChunk); socket.on('fileProcessed', onSocketFileProcessed); function onDocReady() { if(window.File && window.FileReader) { //These are the necessary HTML5 objects the we are going to use document.getElementById('UploadButton').addEventListener('click', onClickStartUpload); document.getElementById('FileBox').addEventListener('change', onFileChosen); } else { document.getElementById('UploadArea').innerHTML = "Your Browser Doesn't Support The File API. Please Update Your Browser"; } } // function onDocReady() function onClickStartUpload() { if(selectedFile !== '') { fReader = new FileReader(); fileName = document.getElementById('NameBox').value; htmlUploading(); fReader.onload = function(evnt) { socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result}); } socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size}); } else { alert("Please Select A File"); } } // function onClickStartUpload() function onFileChosen(evnt) { selectedFile = evnt.target.files[0]; document.getElementById('NameBox').value = selectedFile.name; } // function onFileChosen(evnt) function onSocketNextChunk (data) { // socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct }); var pct = data['Percent']; var chunkNum = data['ChunkNum']; // chunk number (0 <= chunkNum < n; n = fSize/chunkSize) htmlUpdatePct(pct); var nextByte = chunkNum * chunkSize; //The Next Blocks Starting Position var blob = getNextBlob(selectedFile, nextByte, chunkSize); fReader.readAsBinaryString(blob); } // function onSocketNextChunk (data) function getNextBlob(fileObj, startPos, numBytes) { var endPos = startPos + Math.min( numBytes, (fileObj.size - startPos) ); if(fileObj.webkitSlice) return fileObj.webkitSlice(startPos, endPos); else return fileObj.mozSlice(startPos, endPos); } // function getNextBlob(fileObj, startPos, numBytes) function htmlUploading() { var htmlContent = "<span id='NameArea'>Uploading " + selectedFile.name + " as " + fileName + "</span>"; htmlContent += '<div id="ProgressContainer"><div id="ProgressBar"></div></div><span id="percent">0%</span>'; htmlContent += "<span id='Uploaded'> - <span id='MB'>0</span>/" + Math.round(selectedFile.size / mbBytes) + "MB</span>"; document.getElementById('UploadArea').innerHTML = htmlContent; } // function htmlUploading() function htmlUpdatePct(percent) { document.getElementById('ProgressBar').style.width = percent + '%'; document.getElementById('percent').innerHTML = (Math.round(percent * 100) / 100) + '%'; var mbDone = Math.round(((percent / 100.0) * selectedFile.size) / mbBytes); document.getElementById('MB').innerHTML = mbDone; } // function htmlUpdatePct(percent) function onSocketFileProcessed (data) { // socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'}); var htmlContent = "File Successfully Uploaded!" htmlContent += "<img id='Thumb' src='" + data['Preview'] + "' alt='" + fileName + "'><br>"; htmlContent += "<button type='button' name='Upload' value='' id='Restart' class='Button'>Upload Another</button>"; document.getElementById('UploadArea').innerHTML = htmlContent; document.getElementById('Restart').addEventListener('click', onRefresh); document.getElementById('UploadBox').style.width = '270px'; document.getElementById('UploadBox').style.height = '270px'; document.getElementById('UploadBox').style.textAlign = 'center'; document.getElementById('Restart').style.left = '20px'; } // function onSocketFileProcessed (data) function onRefresh() { location.reload(true); } </script> <style type="text/css" media="screen"> body { background: #F9F9F9; font-family: Calibri; font-size: 18px; } h2 { font-size: 40px; margin-top: 6px; margin-bottom: 10px; } #Thumb { max-width: 230px; max-height: 130px; } #ProgressContainer { width: 396px; height: 36px; background: #F8F8F8; margin-top: 14px; border: 1px solid #E8E8E8; border-top: 1px solid #D8D8D8; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; padding: 2px; } #ProgressBar { height: 100%; width: 0%; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.50, #6b0d6b), to(#4a074a)); } #UploadBox { background: #FFF; padding: 20px; position: absolute; top: 50%; left: 50%; margin-left: -200px; margin-top: -150px; height: 200px; width: 400px; border: 1px solid #DFDFDF; -webkit-box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2); -moz-box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2); box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2); -webkit-border-radius: 11px; -moz-border-radius: 11px; border-radius: 11px; } button.Button { font-size: 18px; color: #ffffff; padding: 8px 30px; background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.50, #6b0d6b), to(#4a074a)); -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; border: 1px solid #5b139e; -webkit-box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4); -moz-box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4); box-shadow: 0px 1px 3px rgba(000,000,000,0.5), inset 0px 0px 3px rgba(255,255,255,0.4); text-shadow: 0px -1px 0px rgba(000,000,000,0.1), 0px 1px 0px rgba(145,035,145,1); position: absolute; bottom: 20px; right: 20px; cursor: pointer; } button.Button:hover { background: -webkit-gradient( linear, left top, left bottom, from(#a50aad), color-stop(0.80, #6b0d6b), to(#a50aad)); color: #D3D3D3; } button.Button:active { background: -webkit-gradient( linear, left top, left bottom, from(#4a074a), color-stop(0.80, #6b0d6b), to(#a50aad)); } input { margin-top: 10px; margin-bottom: 8px; } input[type=text] { border: 1px solid #CDCDCD; border-top: 1px solid #676767; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; font-size: 18px; padding: 2px; width: 300px; margin-left: 10px; } </style> </head> <body> <div id="UploadBox"> <h2>Chunked File Uploader</h2> <span id='UploadArea'> <label for="FileBox">Choose A File: </label><input type="file" id="FileBox"><br> <label for="NameBox">Name: </label><input type="text" id="NameBox"><br> <button type='button' id='UploadButton' class='Button'>Upload</button> </span> </div> </body> </html> |
//# -*- mode: javascript; coding: utf-8 -*- var tempDir = 'Temp'; var targetDir = 'Repo'; // mkdir Temp; mkdir Repo; node app.js var mbBytes = 1048576; // bytes in megabyte var chunkSize = 3 * mbBytes; // chunk size in bytes var bufMaxSize = 10485760; // 10 MB var app = require('http').createServer(httpResponder) , io = require('socket.io').listen(app) , fs = require('fs') , exec = require('child_process').exec , util = require('util') , filesList = {}; app.listen(8080); // http://servername:8080/ io.sockets.on('connection', onSocketConnect); function httpResponder(req, res) { fs.readFile(__dirname + '/index.html', function (err, data) { if (err) { res.writeHead(500); return res.end('Error loading index.html'); } res.writeHead(200); res.end(data); } ); } // function httpResponder(req, res) function onSocketConnect(socket) { socket.on( 'fileMeta', function(data) { onSocketFileMeta(data, socket); } ); socket.on( 'fileData', function(data) { onSocketFileData(data, socket); } ); } // function onSocketConnect(socket) function onSocketFileMeta (data, socket) { // start recieve file // socket.emit('fileMeta', {'Name' : fileName, 'Size' : selectedFile.size}); var fName = data['Name'], fSize = data['Size']; var fullName = tempDir + '/' + fName; var fObj = { // Create a new Entry in The Files List fSize : fSize, // file size bytesBuf : '', // data buffer, 10 MB max rcvdBytes : 0, // count of recieved bytes fHandle : '' // file handle } try { // if file exist already var stat = fs.statSync(fullName); if(stat.isFile()) { fObj.rcvdBytes = stat.size; } } catch(err) { // It's a New File console.log(err); } filesList[fName] = fObj; fs.open(fullName, 'a', 0644, function(err, fd) { onFileOpen(err, fd, fName, socket); }); } // function onSocketFileMeta (data, socket) function onSocketFileData(data, socket) { // socket.emit('fileData', {'Name' : fileName, 'Data' : evnt.target.result}); var fName = data['Name']; var blob = data['Data'] var fObj = filesList[fName]; fObj.rcvdBytes += blob.length; fObj.bytesBuf += blob; filesList[fName] = fObj; if(fObj.rcvdBytes == fObj.fSize) { //If File is Fully Uploaded fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) { onFileWriteDone(err, written, buffer, fName, socket); }); } // finish else if(fObj.bytesBuf.length > bufMaxSize) { //If the Data Buffer reaches 10MB fs.write(fObj.fHandle, fObj.bytesBuf, null, 'Binary', function(err, written, buffer) { onFileWriteBuffer(err, written, buffer, fName, socket); }); } else { getNextChunk(fObj, socket); } } // function onSocketFileData(data, socket) function getNextChunk(fObj, socket) { var pct = (fObj.rcvdBytes / fObj.fSize) * 100; var chunkNum = fObj.rcvdBytes / chunkSize; socket.emit('nextChunk', { 'ChunkNum' : chunkNum, 'Percent' : pct }); } // function getNextChunk(fObj, socket) function onFileOpen(err, fd, fName, socket) { var fObj = filesList[fName]; if(err) { console.log(err); } else { fObj.fHandle = fd; // We store the file handler so we can write to it later filesList[fName] = fObj; getNextChunk(fObj, socket); } } // function onFileOpen(err, fd, fName, socket) function onFileWriteBuffer(err, written, buffer, fName, socket) { // buffer writed var fObj = filesList[fName]; fObj.bytesBuf = ''; // Reset The Buffer filesList[fName] = fObj; getNextChunk(fObj, socket); } // function onFileWriteBuffer(err, written, buffer, fName, socket) function onFileWriteDone(err, written, buffer, fName, socket) { var fObj = filesList[fName]; fs.close(fObj.fHandle, function(err) { fObj.fHandle = ''; filesList[fName] = fObj; }); var tempName = tempDir + '/' + fName; var inp = fs.createReadStream(tempName); var out = fs.createWriteStream(targetDir + '/' + fName); util.pump(inp, out, function() { fs.unlink(tempName, function () { socket.emit('fileProcessed', {'Preview' : 'thumbnail.jpg'}); // exec("ffmpeg -i Video/" + Name + " -ss 01:30 -r 1 -an -vframes 1 -f mjpeg Video/" + Name + ".jpg", function(err){ // socket.emit('Done', {'Image' : 'Video/' + Name + '.jpg'}); // }); }); }); } // function onFileWriteDone(err, written, buffer, fName, socket) |