Управляемая раздача файлов через nginx
09 января 2012 :: 13 комментариев :: 33710 просмотров :: 1338 слов

Привет, мои маленькие прогродрузья. Сегодня я расскажу вам то, о чем, как выяснилось, не знают многие веб-прогроммисты. Совсем недавно я переписал свой storage, потому что предыдущий не устраивал меня по нескольким причинам: он был написан мною давным давно на РНР, и по началу вполне устраивал меня, но времена меняются, про РНР я давно забыл, а память, отжираемая apache, стала слишком дефицитным ресурсом, потому что стала требоваться еще многим более важным процессам на сервере. Потому примерно в то же время я перешел на nginx и с тех пор жил счастливо. Пришлось, правда, поебаться со сборкой php-fpm, по крайней мере тогда (2 года назад) под debian даже готовых пакетов с этим добром не было (либо были под древние версии, под etch и sarge). Потому как работает PHP на моем сервере мне всегда не нравилось и я начал избавляться от таких проектов. После глобальной чистки оставалось всего 2 необходимых мне сервиса - это тот самый storage и phpmyadmin. Теперь остался один, да и тот в основном для работы.

Так к чему это я веду. В первой версии сторадж представлял из себя просто файл index.php, который сканировал папку, в которой лежал, разбивал все файлы по категориям, и выводил списком. Скачивались файлы, естественно, по давней традиции тупо по хотлинку. От этой практики я решил тоже отказаться, что уж говорить, 4 года назад я был настолько туп, что первые пару дней даже не фильтровал заливку туда файлов с расширением *.php, а никакого chroot там в помине не было. В общем во второй версии я решил завести для всего этого дела небольшую БД (sqlite вполне подошел) и переписать-таки все на питоне.

Теперь при загрузке файла и всяких очевидных проверок на коллизии имен, он клался в специальную папочку, а запись о нем, сгенерированный ID и еще куча полезных параметров, клались в sqlite. Одной фичей, подсмотренной в одном платном сервисе, был просмотр истории скачиваний, его я тоже захотел себе. Поэтому когда пользователь хочет скачать файл: делается запрос в БД на существование записи и изъятие полного имени файла (файл при заливке немного переименовывается, а при коллизиях - тем более), если все удачно - запрос к диску на проверку существования файла (ну мало ли что бывает), когда и тут все хорошо - запись в историю доступа и, собственно, отдача файла по http.

Стандартно, прямолинейно, ничего нового. Любой, кто работал с файлообменниками, переживал еще большую анальную боль при попытке скачать файл. Сюда можно добавить еще таймеры, капчи, генерацию "временных эксклюзивных url", кнопку "БАБЛО", etc. У нас случай простейший и написать такое под силу даже Студенту АВТФ, успешно сдавшему лабораторную работу по курсу ООП и БД. Но остается один вопрос, описанный в теме поста: как отдать файл пользователю? Я расскажу несколько способов со смешными кодовыми названиями, которые я придумал только что.

1 способ: "долбоеб"

Открыть папку с файлами в паблик через nginx. После всех этих обработок просто редиректнуть юзера на хотлинк на файл.

Плюсы:
1) Звенящая простота и очевидность реализации (отсюда и название)
2) Раздача через nginx

Минусы:
1) Летит к хуям вся система контроля. Один пользователь выложил хотлинк на картинку в твиттере и дальше понятно.

Собственно, этого минуса нам уже достаточно.

2 способ: "долбоеб-педераст"

Похож на предыдущий способ открывания папки в паблик, но с добавлением проверки по рефералу. Если пришел по хотлинку - файл не давать.

Плюсы:
1) Те же, что у предыдущего способа
2) Теперь никто не даст хотлинк

Минусы:
1) Нужна лопатка для соскребания говна, вылитого на тебя конечными пользователями такой системы.
2) Лишний редирект

3 способ: "умный долбоеб-педераст"

А что если про рефералу не банить, а перестроить систему так, чтобы nginx редиректил при неправильном реферале на скрипт, который затем опять редиректил на прямой линк.

Плюсы:
1) Те же, что у предыдущего способа
2) "Наверняка теперь все будет круто"

Минусы:
1) Три изменения url и два редиректа? Да вы ебанулись.
2) А если я захочу отдавать файлы только по ID?

4 способ: "студент АВТФ"

Да ну вас нахуй, давайте читать файл и отдавать его сразу из скрипта. Я по-другому не умею. Я буду читать гигабайтный файл в RAM через open() и затем отдавать как обычный Response(), да еще и заюзаю костыль для определения правильного mimetype.

Плюсы:
1) Охуенно!
2) Решает все проблемы со всеми ссылками.

Минусы:
1) Если повезет и стандартная библиотека вашего любимого языка будет сначала читать весь файл в память, или вы не подумав написали какой-нить return file.realAll(), то вы Хороший Программист!
2) А если не будет и она (скорее всего) умеет читать и отдавать его по-частям, то я вам желаю удачи с пользователями с Дальнего Востока, где интернет по талонам и скорость в кб/с больше похожа на короткий номер службы спасения... вы лишаетесь одного воркера на час. А так как у меня даже самые нужные проекты запускаются с 5-10 воркерами и чувствуют себя отлично, то может наступить пизд^W Bad Gateway или что-то в этом духе.

5 способ: "хабрахабр головного мозга"

Ладно, забудем про рефералы, будем помнить про способ прострелить ногу через file.open и обратимся к профессионалам. Сразу несколько опрошенных мною человек рекомендуют написать модуль для nginx, который будет обрабатывать такие запросы и... WHUT? МОДУЛЬ ДЛЯ NGINX?

Плюсы:
1) Наверное это будет работать

Минусы:
1) Писать модуль, компилить с ним nginx, надеясь, что все заработает, терять поддержку обновлений для всего сервера и постоянно бояться, что что-то сломал?

6 — N способ: "место для вашей рекламы"

Сюда можете придумать еще несколько смешных способов прострелить себе ногу.

Плюсы:
1) Вы придумали его сами или по крайней мере знаете

Минусы:
1) Он подходит только вам и только в вашем конкретном случае

N+1 способ: правильный

Не буду дальше утомлять вас несмешными шутками и скажу волшебное слово: X-Accel-Redirect. Для тех, кому лень открывать ссылку, процитирую:

X-accel allows for internal redirection to a location determined by a header returned from a backend. This allows you to handle authentication, logging or whatever else you please in your backend and then have Nginx handle serving the contents from redirected location to the end user, thus freeing up the backend to handle other requests. This feature is commonly known as X-Sendfile.

Да тут же английским по белому написано как раз то, что нам нужно. Да оно еще и встроено в nginx. Еще, кстати, для пользователей lighttpd (может быть и других серверов), упомянуто, что это аналог их X-Sendfile.

Для самых маленьких и тупых я покажу как это работает.

Структура приложения. Стандартная однофайловая херня.

Для начала сам скрипт, обрабатывающий запросы на скачивание файла. Новый storage написан на werkzeug, но это должно вас мало интересовать, потому что основную часть я все-таки вырежу:

def on_download(self, request, file_id):
# получить имя файла из бд
file = ...

# проверить существование
if file:
# сделать запись в хистори
...
# редиректнуть на прямую скачку файла
response = Response()
response.headers["X-Accel-Redirect"] = "/path_to_files/%s" % file.name
return response
else:
# вернуть ошибку

Простой метод из которого вырезано все не нужное. Мы делаем ровно то, что описывали в начале, а затем, если все хорошо, делаем... да-да, редирект. Вот только обычный редирект, как мы помним, делается через статус-код 3xx хедер Location, а тут через обычный статус 2хх и хедер X-Accel-Redirect. То же на РНР:

header("X-Accel-Redirect: /path_to_file/" . $filename);

Поле данных Response можно оставлять пустым, насрать. Когда запрос обрабатывается, он отдается впередистоящему nginx, который в свою очередь отлавливает заголовок X-Accel-Redirect, удаляет его и дальше делает все сам. Не подменяя URL он заменяет ответ своим. Всякие add_header и gzip_static тоже работают. Кому нужно, можете насильно прописать content_type и отдавать, например, картинки и текст как файлы. А скрипт работу закончил и освободил воркера, как уже отдать файл пользователю решит nginx. У него это получается лучше всего. Как поговаривают в этих ваших интернетах nginx в таком случае даже сам умеет управлять докачкой. Для него это обычный статик-файл.

А как настроить сам nginx? Нужно осилить написать всего 1 лишнюю строчку в location. Как-то так:
location /path_to_files {
root /full/path/to/files;
internal; # <---- вот эту
}

Этот location очень похож на обычный, только вот если вы попробуете обратиться к нему напрямую, то получите 404. Слово internal говорит, что эти файлы являются внутренними и доступны только для самого сервера.

И чо, это работает? Ага:

Сам заголовок, к сожалению, nginx вырезал. А может и к счастью. (Кекеке, "верхние колонтитулы", пошутите кто-нибудь про это)

Комментарии ↓
Nks :: 09 января 2012 в 13:08 из Novosibirsk, RU #
0

Ну, какбэ так и ожидалось. По похапэ у меня вообще работает 2006 года библиотека с этим X-Accel-Redirect не моя и норм отдает через нжингс 3-4 гб файлы (ну я давал ссылку в твиттере). А так - окок. P.S. Как "Request Headers" можно было перевести как "Верхние колонтитулы"? >.<

ВОТ ВЕДЬ ХУЙНЯ!

vas3k :: 09 января 2012 в 13:11 из Novosibirsk, RU #
0

Nks, > ВОТ ВЕДЬ ХУЙНЯ! Да-да, у меня в разрешенных тегах нет картинок :) > Как "Request Headers" можно было перевести как "Верхние колонтитулы"? >.< Литературно!

Nks :: 09 января 2012 в 13:13 из Novosibirsk, RU #
0

V@s3K, >Да-да, у меня в разрешенных тегах нет картинок :) И прально. А то щас бы въебашил бы тебе тут 2000х2000 и срал бы кирпичами :)

ReDetection :: 09 января 2012 в 14:03 из Novosibirsk, RU #
0

300гет, congratulations!

а, в твиттере уже сказали v_v

что-то любой файл отображается как текст, что не так?

vas3k :: 12 января 2012 в 16:43 из Novosibirsk, RU #
0

ReDetection, у тебя или у меня? Не так - это косяк с заголовком ContentType. Посмотри какой он.

ReDetection, ага, увидел, у меня. В номинации "IE 6 года" в этом году точно победит firefox. Я уже выучил все методы создания костылей для этого браузера.

promotion website :: 05 мая 2012 в 23:54 из Moscow, RU #
0

Ухты, посмотрите, полевная штука.

vadipp :: 23 апреля 2013 в 12:48 из Novosibirsk, RU #
0

А по каким соображениям не заопенсорсил?

vas3k :: 23 апреля 2013 в 14:50 из Novosibirsk, RU #
0

vadipp, плохой код. Но конкретно часть с учетом и раздачей файлов перекочевала в i.vas3k.ru, а он на гитхабе есть.

Комментирование доступно только участникам Клуба

Войти
Еще? Тогда вот