wiz 06.02.2011 15:21
Python — Веб-сервер своими руками. Часть 4 — раздача файлов
Переходим к более практической части. Хардкодом побаловались, далее по плану идёт раздача файлов.Я пока смутно представляю какой должна быть реализация, но зато примерно знаю как можно проверить её правильность. Поэтому эти знания мы сейчас выразим в виде теста, к несуществующему пока коду. Такой подход называется Test-driven Development или TDD. Тоесть мы сначала строим измеритель выхлопа, а потом уже на другом конце собираем карбюратор, соответствующий заданым параметрам (= Контринтуитивный манёвр, но частенько помогает определиться с API ещё до написания кода, который потом, во время изменения задачи придётся переписывать. А зачем делать двойную работу?
Обработчики являются как бы плагинами к серверу, он от них никак не зависит, и поэтому должны лежать в отдельном модуле (handlers.py).
Функцию serve_static надо будет импортировать в начале тестов, а в handlers.py сделать заглушку:
1 |
def serve_static(request):
|
Тесты начнут дружно валиться, но теперь понятно что должно быть внутри функции:
Теперь всё проходит. Можно сделать «однострочный веб сервер» для раздачи файлов из текущего каталога в баше, который можно вызывать через `python -m serve`. Пока оно не установлено в пути python из любого каталога конечно не прокатит, но из возле самого сервера работать вполне будет.
1 |
if __name__ == '__main__':
|
Запускаем, пробуем:
1 |
|
Работает. Пробуем дальше:
Ой! К счастью сервер запущен не от рута и /etc/shadow в безопасности. Но сервер при этом жёстко крашнется:
Сразу допишем в test_handlers вредный тест, который будет рушить «сервер» в комфортной обстановке:
1 |
self.server.register(lambda r: r.url == '/crash/me/', lambda r: no_you)
|
Тесты начали фэйлиться с «NameError: global name 'no_you' is not defined». Замечательно, то что нужно.
В каждом хэндлере всех ошибок не отловишь, да и полагаться на их будущих авторов тоже не стоит. «Хочешь чтобы было хорошо — сделай это сам!». Где у нас есть место, в котором можно раз и навсегда защитить сервер от крашей по причине ошибок хэндлеров? Они вызываются из диспетчера on_request, пристегнём его try-мнями безопасности:
Конечно, это не защитит от всяких фатальных ошибок типа вызывающих core dump, но уже что-то. Тесты теперь ловят свой законный «груз 500», но человек, заглянувший браузером останется в непонятках и будет зол. Особенно если это сам разработчик в поисках проблемы. Питон позволяет не просто отлавливать код и сообщение ошибки, но и ситуацию, в которой она возникла. А заполучить это нам поможет штатный модуль traceback.
Сделаем более детальный тест:
1 |
request, headers, body = self.client('/crash/me/')
|
Выражение assert это способ языка проверить очень-важное-условие. assert False — всегда будет вызывать исключение AssertionError. А чтобы сразу было видно, что не понравилось условию, вторым «аргументом» assert идёт тело ответа. Прямо сейчас нам оттуда просто выражают сожаление, но ничего конкретного не сообщают.
Немного изменим обработку ошибки:
1 |
|
curl стал показывать трейсбек и ошибку, что значительно облегчит написание и отладку обработчиков:
1 |
...
|
Этому хаку уже сто лет 21 год в обед и нам ещё повезло, что open не умеет выполнять команды внутри скобок типа `mail [email protected] < /etc/shadow` и прочие шелловские штучки, которым были подвержены в детском возрасте многие демоны. Но всё же, даже такая штука весьма неприятна даже если сервер запущен от nobody:nogroup.
Сегодня — день TDD, поэтому сразу заготавливаем проверку. Это уже не для сервера в целом, а для конкретного хэндлера, поэтому и отправляется в его набор test_static:
1 |
|
Убедившись, что nosetests выдаёт наш «законный» AssertionError: '404' != '200', отправляемся писать фикс.
В библиотеке os.path есть много интересных функций, поэкспериментировав с которыми можно найти одну, которая выдаёт, что указаный путь выходит за уровень начального:
Что-то я уже подзадолбался писать «return request.reply()», тем более, что сервер в итоге и так ещё потом пытается ловить ошибки. А ведь 4хх и 5хх это именно ошибки и есть. Вынесем их в отдельный класс исключений, которые затем можно будет везде бросать и ловить:
1 |
|
И всё. Остальные могут его импортировать и пользоваться. Сделаем для него специальный способ обработки в диспетчере:
После перевода ошибок на рельсы HTTPError оставшиеся ошибки из файловой системы транслируются в коды HTTP достаточно тривиально.
Выдавать файлы из текущего каталога это забавно, но хочется всё же указать привычный /var/www, а может даже не один. Тоесть понадобится система альясов url ? path, а значит нужна связь между паттерном и хэндлером. Можно было бы сделать специальный класс с конструктором (url, root) и методами pattern & handler, которые через экземпляр класса бы знали свои начальные параметры. Но «объекты это замыкания для бедных» © Norman Adams. Действительно, зачем городить целый класс, потом создавать его экземпляр, потом брать его методы и засовывать в сервер, когда нам нужно всего лишь объединить область видимости уже готовой функции с паттерном, который отловит соответствующий URL.
Первым делом, первым делом — юнит тесты... Завернём раздатчик статики так, чтобы функция выдавала две других функции и передадим это хозяйство через развёртывание позиционных аргументов:
1 |
|
Ровно тоже самое, что и было: все урлы раздаются из текущего каталога.
Нынешнюю serve_static переименуем в handler и завернём внутрь новой serve_static(url, root):
1 |
|
Теперь у нас есть создание функции внутри другой функции. При этом изнутри handler будут доступны также ещё url и root из «родительской функции». Добавим паттерн и возврат двух свежесозданных функций:
Замыкания потому так и называются, что, после того, как возврат произведён, всё состояние как бы перестаёт существовать, как река, в которую нельзя войти дважды. Но функции, созданные (они на самом деле создаются на ходу - можете проверить их id) продолжают иметь доступ к этому «висящему в пустоте» контексту и всем его переменным. Поэтому единожды посчитаная длина базового URL продолжает оставаться доступной для всего кода порождённого внутри.
Тесты как обычно показывают, что код в порядке и при переезде ничего не отломилось.
Другой важной задачей файлого сервера, помимо отдавания файлов, является... не-отдавание файлов когда это возможно. Это называется «кэш на стороне клиента» и обеспечивается с помощью пары заголовков If-Modified-Since/Last-Modified и кода HTTP 304: Not modified.
Для того чтобы договориться о том, что надо или не надо передавать содержимое файла сервер посылает заголовок Last-Modified с датой последнего изменения по GMT в формате RFC1123: '%a, %d %b %Y %H:%I:%S GMT'. Чтобы получить эту дату (и ещё другие вещи, которые тоже пригодятся) используем os.stat:
Отсюда нам интересны st_mtime (время изменения), st_size (размер) и st_mode (вдруг это не файл вообще). Если такого узла в файловой системе не будет, возникнет исключение OSError/2 (а не, IOError, как в случае с open). С каталогами мы разберёмся в следующей части, а дата и размер берутся достаточно легко.
Клиент, увидев дату, запомнит её и в следующий раз отправит вместе с запросом в заголовке If-Modified-Since, который надо распарсить и сравнить с датой файла. Сразу оформим это в виде теста:
И сделаем закорачивание обработки сразу после stat:
Вот и всё. Осталось починить дефолтный сервер, а заодно добавить немного настроек.
В python 2.7 появился очередной модуль разбора аргументов командной строки argparse, который довольно неплох. Для предыдущих версий его можно поставить из pip. А можно и не ставить и сделать так, чтобы сервер запускался и без него. import ничем не хуже других инструкций питона и бросает самые обычные исключения, которые можно просто отловить и обойти.
Код теперь попытается разобрать командную строку и скромно откажется принимать какие-либо аргументы кроме --help/-h. Допишем разбор порта и корневого каталога для раздачи:
Формат очень простой: полное имя переменной (короткое он сам сделает), количество аргументов (? - один необязательный, * - много необязательных или фиксированое число), в какой тип преобразовывать (если надо) и значение по-умолчанию (None, если не указывать). Там есть ещё другие интересные опции, но этих нам хватит. Если полное имя написать без «--», то получится позиционный аргумент.
Результаты регистрации аргументов можно посмотреть вызывав справку:
1 |
$ python -m serve -h
|
Даже такая, казалось бы, простейшая задача содержит предостаточно подводных граблей и возможностей хвастнуть питоном. В следующей статье мы обкатаем ещё несколько важных особенностей сервера, протоколов и питона.
(весь код в сборе)
UNIX is the IDE (;
Все статьи писались в gedit. Свои проекты на удалённых дев-серверах корячу в емаксе и то только из-за копипаста и подсветки.
Все статьи писались в gedit. Свои проекты на удалённых дев-серверах корячу в емаксе и то только из-за копипаста и подсветки.
Ясно, спасибо :) Пойду в гугл, пошукаю что-нибудь адекватное для Vim :)
найдёшь вдруг - будь добр свистни %) а то я не нашёл ничего, кроме сниппетов :))
Если хочется немного поиграться то можно bpython.
Интерпретатор Python с подсветкой, автодополнением и документацией.
Интерпретатор Python с подсветкой, автодополнением и документацией.
Есть ещё ipython, тоже неплох. Особенно мне нравится, что можно автодополнять выражение
from module import <и тут по табу автодополняются импортируемые из модуля объекты>
В bpython я такого вроде не видел.
from module import <и тут по табу автодополняются импортируемые из модуля объекты>
В bpython я такого вроде не видел.
Из бесплатных решений неплохо зарекомендовал себя eclipse + pydev.
Из платных можно выделить Wing IDE, pycharm, komodo.
А здесь преставлен широкий список с небольшим описанием: http://wiki.python.org/moin/IntegratedDevelopmentEnvironments, http://wiki.python.org/moin/PythonEditors
Из платных можно выделить Wing IDE, pycharm, komodo.
А здесь преставлен широкий список с небольшим описанием: http://wiki.python.org/moin/IntegratedDevelopmentEnvironments, http://wiki.python.org/moin/PythonEditors
Кстати, как статьи по объёму? 200± строк как сейчас это ок или много? Или может можно смело до 400 догонять?
200 норм, но 400 уже сложно переварить за раз будет - всё-равно частями чиататься будет :)
Афтар, пеши есчо :)
А серьезно - жжошь, так держать!
А серьезно - жжошь, так держать!
Кстати, wiz, посоветуйте пожалуйста IDE для Python. Eric почему-то вызывает отторжение.