понедельник, 15 апреля 2013 г.

HH: Загрузка файлов на чистом nginx

копипаста: оригинал на хабре удален, видимо вместе с автором.
Захотелось мне написать файлообменник (для личных нужд, с нуля), да не простой, а с красивым прогресс-баром, — с отображением процесса загрузки файлов на сайт.
И остановился я на чисто серверном решении nginx с модулями nginx-upload и nginx-upload-progress.

nginx не нуждается в описании; nginx-upload управляет процессом загрузки данных, который затем может передать результат на бэкенд, — ваш PHP скрипт (или еще куда); nginx-upload-progress же отвечает исключительно за информирование о процессе загрузки, — все написано в документации, но вкратце лишь скажу, что со страницы выполняется ajax-запрос на сервер со специальным http-заголком, в котором хранится уникальный id, и в ответе по этому id мы можем узнать состояние загрузки данных.

Но а теперь, хочу поделиться с вами, как легко и просто организовать загрузку файлов с прогресс-баром. С нуля. Для нетерпеливых, что должно получиться: загрузка нескольких файлов сразу, определение скорости, времени и рисование самого прогресс-бара.

Установка


Скачиваем, распаковываем и переименовываем (для чистоты) оба модуля.

$ wget http://www.grid.net.ru/nginx/download/nginx_upload_module-2.2.0.tar.gz $ wget -O upload_progress.tar.gz https://github.com/masterzen/nginx-upload-progress-module/tarball/v0.8.4 

$ tar xf nginx_upload_module-2.2.0.tar.gz $ tar xf upload_progress.tar.gz 

$ mv nginx_upload_module-2.2.0/ nginx-upload $ mv masterzen-nginx-upload-progress-module-82b35fc/ nginx-upload-progress $ rm nginx_upload_module-2.2.0.tar.gz upload_progress.tar.gz


И прежде всего, нам нужно пересобрать nginx для добавления этих двух модулей. У всех дистрибутивы с пакетными менеджерами разные, каждый делает это по-своему… В любом случае, вам нужно только добавить два параметра к ./configure и запустить сборку:

./configure \ ... --add-module="../nginx-upload" \ --add-module="../nginx-upload-progress"


Настройка


Когда nginx собрался и готов к запуску, приступаем к настройке модулей. Вот рабочий пример моего конфига:

... events { ... } http { ... # подключаем nginx-upload-progress модуль, называем его "upload" upload_progress upload 2m; server { # все стандартно listen localhost; server_name localhost; root /srv/http/localhost; # настройка nginx-upload модуля # сюда будут отправляться данные из POST форм location = /upload/share { # увеличиваем лимит на размер загружаемых данных client_max_body_size 250m; # указываем бэкенд, который выполнится уже после загрузки данных # это может быть ваш PHP скрипт для управления файлами # и директорию, куда сохраняются загруженные файлы upload_pass /upload; upload_store /tmp; # укажем, какие дополнительные данные передать бэкенду upload_set_form_field $upload_field_name.name "$upload_file_name"; upload_set_form_field $upload_field_name.content_type "$upload_content_type"; upload_set_form_field $upload_field_name.path "$upload_tmp_path"; upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5"; upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size"; # в случае возникновения этих ошибок файлы будут удалены upload_cleanup 400 404 499 500-505; # урезаем скорость # это мне необходимо для долгой загрузки файлов # чтобы дебажить скрипт и успеть налюбоваться на процесс загрузки upload_limit_rate 8k; # включаем информирование для "upload" (см. в начале) track_uploads upload 1m; } # сюда приходят ajax-запросы со страницы location = /upload/status { # информируем их о процессе загрузки report_uploads upload; } } }


Запуск


И осталось только нарисовать красивую форму для загрузки файлов… Ну, как сказать, «красивую». Программист — не дизайнер, сами понимаете.

cat << EOF > /srv/http/localhost/index.html
Скрытый текст
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <script> function add() { if (parseInt(document.getElementById('count').getAttribute('value')) < 8) { var input = document.createElement('input'); input.setAttribute('type','file'); input.setAttribute('multiple',''); input.setAttribute('name','file[]'); document.getElementById('multiple').appendChild(input); document.getElementById('multiple').appendChild(document.createElement('br')); document.getElementById('count').setAttribute('value',parseInt(document.getElementById('count').getAttribute('value'))+1); } else { alert('Можно загрузить не более 8 файлов за раз.'); } } function progress() { var ms = new Date().getTime() / 1000; rq = 0; id = ""; for (i = 0; i < 32; i++) { id += Math.floor(Math.random() * 16).toString(16); } document.getElementById('upload').action = "/upload/share?X-Progress-ID=" + id; document.getElementById('status').style.display = 'block' interval = window.setInterval(function () { fetch(id, ms); }, 1000); return true; } function fetch(id, ms) { var fetch = new XMLHttpRequest(); fetch.open("GET", "/upload/status", 1); fetch.setRequestHeader("X-Progress-ID", id); fetch.onreadystatechange = function () { if (fetch.readyState == 4) { if (fetch.status == 200) { var now = new Date().getTime() / 1000; var upload = eval(fetch.responseText); if (upload.state == 'uploading') { var diff = upload.size - upload.received; var rate = upload.received / upload.size; var elapsed = now - ms; var speed = upload.received - rq; rq = upload.received; var remaining = (upload.size - upload.received) / speed; var uReceived = parseInt(upload.received) + ' bytes'; var uDiff = parseInt(diff) + ' bytes'; var tTotal = parseInt(elapsed + remaining) + ' secs'; var tElapsed = parseInt(elapsed) + ' secs'; var tRemaining = parseInt(remaining) + ' secs'; var percent = Math.round(100*rate) + '%'; var uSpeed = speed + ' bytes/sec'; document.getElementById('length').firstChild.nodeValue = parseInt(upload.size) + ' bytes'; document.getElementById('sent').firstChild.nodeValue = uReceived; document.getElementById('offset').firstChild.nodeValue = uDiff; document.getElementById('total').firstChild.nodeValue = tTotal; document.getElementById('elapsed').firstChild.nodeValue = tElapsed; document.getElementById('remaining').firstChild.nodeValue = tRemaining; document.getElementById('speed').firstChild.nodeValue = uSpeed; document.getElementById('bar').firstChild.nodeValue = percent; document.getElementById('bar').style.width = percent } else { window.clearTimeout(interval); } } } } fetch.send(null); } </script> </head> <body> <form method="post" enctype="multipart/form-data" id="upload" onsubmit="progress();"> <input type="hidden" id="count" value="1" /> <div id="multiple"> <input type="file" name="file[]" multiple /><br> </div> <input type="submit"> <a href="#" onclick="add();">add();</a> </form> <div id="status" style="display: none;"> <table width="100%"> <tr><th></th><th>загрузка</th><th>осталось</th><th>всего</th></tr> <tr><td>время:</td><td id="elapsed">∞</td><td id="remaining">∞</td><td id="total">∞</td></tr> <tr><td>размер:</td><td id="sent">0 b</td><td id="offset">0 b</td><td id="length">0 b</td></tr> <tr><td>скорость:</td><td id="speed">n/a</td></tr> </table> <div style="border: 1px solid #c0c0c0;"> <div style="background: #c0c0c0; width: 0%; text-align: right;" id="bar">0%</div> </div> <a href="#" onclick="if (confirm('Вы точно хотите отменить загрузку?')) window.location = '/'" id="cancel">cancel_upload();</a> </div> </body> </html>
EOF

Переходим на http://localhost/index.html, пробуем загрузить файлы… Прогресс-бар работает! Теперь дело за малым, написать сам бэкенд, который будет выполняться уже после загрузки файлов на сервер и управлять ими, и нарисовать какой-никакой дизайн для файлообменника.
И с этим, пожалуй, думаю вы справитесь сами, так как моей целью было лишь показать серверную реализацию загрузки файлов с информированием о самом процессе загрузки.

4 комментария:

  1. Было бы мило, если бы вы исправили форматирование кода, ибо в данный момент он выглядит как одна длинная строка :)

    ОтветитьУдалить
    Ответы
    1. В хроме цвета конечно вырвиглаз, но показывает нормально. Или имеются в виду конфиги? Так было в источнике копипасты. Конфиг переформатирую, как будет время.
      Или надо найти автора и запросить оригинал, тогда обновлю целиком всё.

      Удалить
  2. как раз таки "воды" тут больше чем чего либо, так как понимания о чем пишете 0.

    ОтветитьУдалить
    Ответы
    1. 1) > копипаста: оригинал на хабре удален, видимо вместе с автором.
      Первая же строка. Да, и форматирование кривое, прямее копии мне найти не удалось.
      2) все наезды к автору
      3) пару моментов мне показались годными, умный человек всё-равно 1 в 1 передирать не будет, а будет сам делать, только подглядывая.
      Хотя статья может уже вообще неактуальная, что-то было в рассылке nginx про аплоад модули, и вполне вероятно что nginx-upload уже не рабочий давно. Статье больше года.

      Удалить