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

в 9:30, , рубрики: ajax, nginx, upload, Веб-разработка, метки: , ,

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

nginx не нуждается в описании; nginx-upload управляет процессом загрузки данных, который затем может передать результат на бэкенд, — ваш PHP скрипт (или еще куда); nginx-upload-progress же отвечает исключительно за информирование о процессе загрузки, — все написано в документации, но в кратце лишь скажу, что со страницы выполняется ajax-запрос на сервер со специальным http-заголком, в котором хранится уникальный id, и в json-ответе по этому 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, пробуем загрузить файлы… Прогресс-бар работает! Теперь дело за малым, написать сам бэкенд, который будет выполняться уже после загрузки файлов на сервер и управлять ими, и нарисовать какой-никакой дизайн для файлообменника.
И с этим, пожалуй, думаю вы справитесь сами, так как моей целью было лишь показать серверную реализацию загрузки файлов с информированием о самом процессе загрузки.

Автор: Spoofing

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js