Отдаем статические файлы с помощью Nginx и Rails
В легаси проекте на Rails столкнулись с проблемой раздачи pdf файлов, которая была реализована через send_file
.
(никогда так не делайте!!!)
Рельса не рассчитана на раздачу файлов или какого-нибудь медиа-контента, когда кто-то начинал скачивание довольно большого файла, тред блокировался и ждал окончания загрузки, в итоге однопоточное приложение не позволяло другим работать.
Попытка сменить thin на puma, не увенчалась успехом, ибо у нас и девелопмент и продакшен на Windows Server + Helicon Zoo + Ruby 1.9.3 (да, я страдаю от этого, ничего не поделать, проект легаси, тестов нет и переводить его на другую платформу или версию не представляется возможным).
Нужно было эффективное и к тому же простое решение в лоб. Зная, что Nginx умеет раздавать файлы и не только, решил взять его.
Ну хватит предыстории, перейдем к делу.
Скачивам nginx с nginx.org основную версию и распаковываем её на C: диск.
В случае с дистром линукса (у меня Debian testing) обойдемся одной командой sudo apt-get install nginx
Перейдем в nginx-1.9.15/conf/
и откроем nginx.conf
По умолчанию выглядит он так: (я удалил некоторые закомментированные строки)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; keepalive_timeout 65; server { listen 85; server_name localhost; access_log logs/host.access.log main; location / { root html; index index.html index.htm; } # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } |
Включим логи для просмотра запросов к нашим файлам, раскомментируем строчки, которые начинаются на log_format
и access_log
(11 и 15 строки).
У Nginx уже идет с замечательным модулем Secure Link module, так что нам не нужно вручную собирать.
Определим новый путь (data
) к pdf файлам:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | location /data/ { types { application/octet-stream .pdf; } add_header Content-disposition "attachment; filename=$arg_name"; set $security_key 'some'; secure_link $arg_hash,$arg_expires; secure_link_md5 "$secure_link_expires$uri $security_key"; if ($secure_link = "") { return 403; } if ($secure_link = "0") { return 410; } alias "//PC-ALEX/PdfFiles/$arg_ts/"; } |
Работа модуля заключается в генерации хэша и в сравнивании поступающего через query url params хэша.
Сравниваются $secure_link
и $secure_link_md5
и результат их сравнения поступает в $secure_link
, если значение пусто, тогда хэши не равны, нулевое значение говорит нам, что об истечении срока жизни ссылки, и только если они равны отдает файл. Чтобы сгененрировать защищенный хэш, нужно объявить $security_key
тут и в коде руби.
Определим путь до папки с файлами:
alias "//PC-ALEX/PdfFiles/$arg_ts/";
, где $arg_ts
, путь к подпапкам этой директории, который определяется динамически.
$arg_expires
- timestamp, определяет время жизни ссылки, передается через url параметры.
Отдаем файлы как attachment:
types { application/octet-stream .pdf; }
add_header Content-disposition "attachment; filename=$arg_name";
Хорошо, теперь нужно сделать из nginx сервис, для этого качаем NSSM Указываем название сервиса и путь до сервера.
.\nssm install "Nginx"
Запускаем сервис.
Сгенерируем хэш и нужный url на Ruby, думаю тут всё понятно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class NginxUrlGenerator SECURITY_KEY = 'some' LIFETIME_SEC = 3 HOST = 'http://localhost:85' NGINX_PATH_MAP = { inline: 'inline', attachment: 'data' } attr_reader :file, :disposition, :file_path, :remote_ip, :name def initialize(file_path, file_name, disposition, request, name) @file_path = file_path @disposition = NGINX_PATH_MAP[disposition.to_sym] @file = "/#{@disposition}/#{file_name}" @remote_ip = request.remote_ip @name = name end # Обязательно нужен utc timestamp def timestamp_with_lifetime Time.now.utc.to_i + LIFETIME_SEC end def md5_hash Digest::MD5.digest("#{timestamp_with_lifetime}#{file}#{remote_ip} #{SECURITY_KEY}") end def base64_hash Base64.encode64(md5_hash).gsub(/\+/, '-').gsub(/\//, '_').gsub(/\=/, '') end def full_file_path "#{HOST}/#{file}?hash=#{base64_hash}&ts=#{file_path}" + "&expires=#{timestamp_with_lifetime}&name=#{name}" end end |
Использование:
1 2 3 4 5 6 | # 201605098 - подкаталог link = NginxUrlGenerator.new('20160509','25d044d3896a.3.pdf','attachment',request,'file_name') redirect_to link.full_file_path |
Вот как бы всё. Полный конфиг можно найти по ссылке ngixn.conf
Спасибо за внимание! :)