Кто о чем, а
вшивый о бане. В продолжение этого
поста: возобновляемая закачка файлов
на веб-сервер с использованием HTML5
FileAPI + Node.js + Socket.io
я проверил
работоспособность предлагаемого решения
Заодно и код
причесал (см.полный листинг в конце
поста)
Кому лень
читать многабукаф, заявляю сразу —
предлагаемое решение вполне работоспособно.
Если нужен веб-сервис по надежной,
возобновляемой заливке мега(гига)файлов
недорого — берите, пользуйтесь.
Остальные
читают дальше :)
В результате
получилась как бы инструкция по созданию
веб-сервиса по заливке файлов практически
любого размера на сервер. На самых
современных технологиях :)
Для создания
сервиса мне понадобится:
- виртмашина с Debian testing на борту, это будет сервер;
- браузер с поддержкой HTML5 File API (Google Chrome 19);
- файл гигов на 5 и более для тестирования.
Виртмашина с дебианчиком
Со страницы
debian.org/devel/debian-installer
забрал образ
debian-wheezy-DI-a1-i386-netinst.iso
и поставил из
него систему в виртуалбоксе. Выделил 2
ядра и 512 мегабайт оперативки.
Свежепоставленную
систему доточил напильником:
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
Веб-сервер
Node.js ставится из исходников традиционным
образом. Я собрал и запустил ноду по
инструкции с сайта разработчиков
github.com/joyent/node/wiki/Installing-Node.js-via-package-manager
# 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
Это будет
модуль к Node.js. Установка модуля проста
до безобразия
$ mkdir -p ~/node && cd $_ $ npm install socket.io # после заливки программ на сервер, запустить тут веб-сервер # node app.js |
Типа, всё,
сервер готов. Дальше пишем программу.
Код программы
см.в самом
низу. Здесь приведу только сниппеты.
Еще раз — код не мой а Габриэля
Манрика, я его творение только слегка
переписал. CSS & HTML вообще не трогал.
Файлов два:
один — это HTML для скармливания браузеру;
второй — 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
После успешного
запуска я провел тестирование на предмет
— как оно в целом. Выяснилось, что в
целом — неплохо. За исключением того,
что очень долго выполняется финальная
стадия, а именно перенос закачанного
файла из папки Temp в папку Repo. Этот кусок
кода надо переделать.
В общем, файл
размером 8621 мегабайт был передан успешно,
со средней скоростью 3.5 мегабайта в
секунду. Этот же файл через rsync залился
со скоростью 25 мб/сек. Откуда берется
такое замедление — неясно, задача почти
не дает нагрузки на ресурсы компа.
Браузер (Chrome
19) сожрал 1 гигабайт вирт.памяти, 128
реальной оперативки, 70% процессора
(одного ядра). Утечек памяти нет.
Node.js сожрала
140 мегабайт вирт.памяти, 90 реальной, 40%
процессора (одного ядра). Утечек памяти
нет. В сумме, четырехядерный процессор
был занят на 15%.
Файл передавался
чанками по 3 мегабайта; серверный буфер
10 мегабайт, после его заполнения буфер
сбрасывается на диск.
Финальный
сброс буферов и перенос файла из временной
папки в репозиторий занял около 3-х
минут.
Также я проверил
возобновление прерванной закачки.
Работает как и ожидалось — ни падение
браузера, ни падение сервера не мешает
продолжить закачку с того места, где
остановились в прошлый раз.
Вывод.
Технология
рабочая, рекомендую к использованию.
Полный код аплоадера
index.html
<!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> |
app.js
//# -*- 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) |
Комментариев нет:
Отправить комментарий