Пользователь
0,0
рейтинг
2 сентября 2014 в 09:45

Разработка → Сервис загрузки файлов на Golang

Go*
В ходе разработки серверной части сервиса загрузки файлов на Golang родилось отдельное приложение – pavo. В задачи приложения входит загрузка целых файлов, по одному или несколько за раз, кусочная загрузка файла(chunked upload), конвертер изображений. Реализована загрузка данных через multipart/form-data и загрузка файла в бинарном виде в теле запроса. Для работы в production окружении используется nginx для авторизации и обработки медленных соединений. В качестве клиентской библиотеки можно использовать jQuery File Uploader.

Установка


Установка компилятора

Для установки приложения необходим компилятор Golang. Инструкцию по его установке можно найти на официальном сайте. Так же необходимо настроить переменную окружения $GOPATH.

Для примера, как это можно сделать в MacOS:
  1. Устанавливаем компилятор;

    $ brew install go
    $ mkdir $HOME/go
  2. Настраиваем переменную окружения, отредактировав профайл пользователя.

    # Add this line in your .zshrc or .bash_profile
    export GOPATH=$HOME/go
    export PATH=$PATH:$GOPATH/bin
    


Установка систем контроля версиями

Репозитории с кодом в сообществе не централизованы. Используются различные системы контроля версий:

Пример установки в MacOS:

$ brew install git mercurial svn bazaar


Установка ImageMagick

Для конвертации изображений на сервер используется ImageMagick:

$ brew install imagemagick


Установка приложения

При первой установке запустите команду в консоли:

$ go get github.com/kavkaz/pavo

При обновлении приложения и зависимых библиотек:

$ go get -u github.com/kavkaz/pavo/...


Быстрый старт


Для того, чтобы посмотреть, как работает приложение с базовым примером, запустим в консоли команду:
$ pavo --storage=$GOPATH/src/github.com/kavkaz/pavo/dummy/root_storage

Тем самым мы запустили приложение с корневой директорией в указанном через опцию --storage каталоге. Сервис c базовым примером будет доступен по адресу localhost:9073/example/jfu-basic.html. Для указания другого хоста и порта используйте консольную опцию --host.

Подробности протокола


Типовой ответ сервера при загрузке изображения:

{
    "files": [
        {
            "dir": "/image/2014/6s/1c5cnx",
            "name": "original_user_filename.jpg",
            "type": "image",
            "versions": {
                "original": {
                    "filename": "original-1qeh.jpg",
                    "height": 420,
                    "size": 28057,
                    "url": "/image/2014/6s/1c5cnx/original-1qeh.jpg",
                    "width": 300
                },
                "thumbnail": {
                    "filename": "thumbnail-1qef.jpg",
                    "height": 90,
                    "size": 3566,
                    "url": "/image/2014/6s/1c5cnx/thumbnail-1qef.jpg",
                    "width": 120
                }
            }
        }
    ],
    "status": "ok"
}

Самый распространённый способ загрузки файлов на сервер – использование форм. В таком случае запрос представляется следующим образом:
POST /files HTTP/1.1
Content-Length: 21929
Content-Type: multipart/form-data; boundary=----5XhQf4IXV9Q26uHM

------5XhQf4IXV9Q26uHM
Content-Disposition: form-data; name="files[]"; filename="pic.jpg"
Content-Type: image/jpeg

...bytes...

В заголовке Content-Type передаётся значение boundary, которое служит для разделения значений в теле запроса. Таким образом за один запрос можно передать несколько файлов. jQuery File Upload имеет соответствующую опцию, для множественной отправки файлов.

Современный подход позволяет отправлять бинарные данные, используя на клиенской стороне запрос типа XHR. Запрос, который увидит сервер, выглядит следующим образом:
POST /files HTTP/1.1
Content-Length: 21744
Content-Disposition: attachment; filename="pic.jpg"

...bytes...

Таким способом можно передать за один запрос только один файл, имя которого будет доступно в заголовке Content-Disposition.

Для загрузки больших файлов на клиентской стороне формируется пачка запросов с частями исходного файла. Пример запроса:
POST /files HTTP/1.1
Content-Length: 10240
Content-Range: bytes 0-10239/36431
Content-Disposition: attachment; filename="pic.jpg"
Cookie:pavo=377cb76c-2538-40d3-a3d0-13d86d206ba7

...bytes...

Имя исходного файла и значение cookie по ключу pavo используется для идентификации промежуточного файла с загруженными частями оригинала. Заголовок Content-Range содержит информацию о том, какую часть файла предаёт клиент и каков размер оригинального файла. Если загружается последний кусок, то сервер завершает процедуру загрузки и формирует ответ с данными о полученном файле и его версиях.

Код приложения


Приложение написано на языке Golang. В качестве веб-фреймворка используется Gin. Код разделен на два пакета(upload и attachment) и основое приложение (исполняемый файл). Пакет upload отвечает за загрузку исходных файлов или куска файла. Пакет attachment отвечает за создание конечной директории для хранения файла и его версий, конвертацию изображений, формирование данных. Основное приложение запускает веб-сервер и реализует роль контроллера.

Исходный код с небольшими примера в тестах доступен на github.

Опции приложения


Приложение имеет опции запуска --host и --storage. Указывают host:port для запуска веб сервера и корневую директорию хранилища соответственно.

Все запросы на загрузку приложение принимает на адрес /files. В query_string параметре converts можно передать параметры конвертации для изображений. Например:

POST /files?converts={"pic":"400x300"}

Для всех файлов устанавливается версия по умолчанию original. Для изображений добавляется thumbnail со значением 120x90.

Production environment


Для работы в production окружении желательно использовать веб сервер nginx. В задачи веб сервера будет входить приём запросов от клиентов, запись тела во временный файл, авторизация запроса на основном приложении, отправка заголовков исходного запроса на приложение pavo.

Рекомендуемая конфигурация nginx:

server {
    listen 80;
    server_name pavo.local;
    
    access_log /usr/local/var/log/nginx/pavo/access.log;
    error_log /usr/local/var/log/nginx/pavo/error.log notice;
    
    location /auth {
        internal;
        proxy_method GET;
        proxy_set_header Content-Length "";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass_request_body off;
        proxy_pass http://localhost:3000/auth/url/in/your/app;
        client_max_body_size 0;
    }

    location /files {
        auth_request /auth;
    
        client_body_temp_path     /tmp;
        client_body_in_file_only  on;
        client_body_buffer_size   521K;
        client_max_body_size      10G;
    
        proxy_pass_request_headers on;
        proxy_set_header X-FILE $request_body_file;
        proxy_pass_request_body off;
        proxy_set_header Content-Length 0;
        proxy_pass http://127.0.0.1:9073;
    }
    
    location / {
        root /Path/To/Root/Of/Storage;
    }
}


От клиента на веб сервер приходит запрос. Nginx дожидается получения всего запроса (эта его особенность не позволяет реализовать полноценный progress bar с проксированием на сервер приложения). После получения запроса тело будет записано во временный файл в директорию, определенную опцией client_body_temp_path.

Перед отправкой запроса на сервер приложения pavo будет произведена авторизация. Для этого используется модуль ngx_http_auth_request_module. Будет сделан подзапрос на location /auth, который в свою очередь проксирует заголовки исходного запроса на сервер основного приложения. В случае успешной авторизации сервер должен вернуть пустое тело с кодом статуса ответа 200.

Далее в заголовки исходного запроса добавляется новая пара, ключ X-File, а значение – путь до временного файла с телом запроса. И только после этого получившийся запрос (заголовки и пустое тело) отправляется на прилоежение pavo. Оно обрабатывает запрос, сохраняя файлы, и возвращает ответ с данными о загруженных файлах в JSON формате.

Заключение


Сервис задумывался как самостоятельное приложение в инфраструктуре веб проекта, которое берёт на себя роль загрузки и раздачи файлов, конвертации изображений, видео и аудио. С интерфейсом через HTTP Json API.

UPDATE: Исправлены настройки заголовков проксируемых запросов в конфигурации nginx.
Заур Абасмирзоев @Kavkaz
карма
44,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (7)

  • 0
    multipart/form-data позволяет вместе с файлом отправить и другие данные, составляющие комплексный запрос. К тому же HTML5 умеет создавать FormData объекты, не привязанные к DOM, которые благополучно скармливаются jQuery.
    • 0
      Но при этом multipart/form-data должен кто-то парсить, чтобы вытаскивать от туда файл и другие данные. Так что уже не получится схемы, когда сохранением файла занимается исключительно nginx.
  • +1
    В location /files видимо должно быть тоже proxy_pass_request_body off;, чтобы не передавать тело запроса, а только путь к файлу.
    • 0
      Да, стоит добавить. Тогда уже и proxy_set_header Content-Length "";
      • 0
        Только 0, а не "".
        • 0
          В доках написано использовать пустую строку. На самом деле работает и то, и то.
          • +1
            Там так написано потому, что ещё задано proxy_method GET;. Это важный момент, поскольку у GET запросов, как известно, Content-Length быть не должно. В частности, у вас для location /auth установка proxy_set_header Content-Length 0; некорректна, поскольку auth_request тоже посылает GET запрос.

            Если не менять тип запроса, то для POST запросов без тела лучше использовать значение 0, а не спиливать заголовок совсем. В противном случае бекенд будет вынужден определять окончание запроса по закрытию соединения, что не очень оптимально и делает невозможным включение keep-alive в этом месте.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.