Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- coding: utf-8 -*-
- import base64
- from twisted.web import server, resource, client
- from twisted.internet import reactor, error
- from twisted.python import log
- from twisted.enterprise import adbapi
- # Настройки для соединения с БД я вынес отдельно
- db_options = {
- "database": "werehuman"
- }
- # Нам нужна всего одна табличка
- # CREATE TABLE urls (
- # id SERIAL PRIMARY KEY,
- # link VARCHAR(512) NOT NULL);
- # Никто не запрещает использовать шаблонизатор
- # Но для примера это будет жирновато
- html = """<!DOCTYPE html>
- <html>
- <head><title>Twisted URL Shortener</title></head>
- <body style="padding: 50px">
- <div style="font-size: 2em; margin-bottom: 2em">Простая сокращалка ссылок</div>
- <div>
- %s
- </div>
- <div style="margin-top: 10em"><a href="/">Вернуться на глагне</a><br /><a href="/source/">Залезть под капот</a></div>
- </body>
- </html>"""
- error_html = html % "Ой! ☹ Произошла ошибка: %s"
- # Ресурс, который будет выступать в качестве главной страницы
- # Он же будет редиректить на ссылки
- class Index(resource.Resource):
- isLeaf = False # Означает, что к этому ресурсу будут подсоединены еще ресурсы (см. ниже)
- def getChild(self, name, request):
- # http://twistedmatrix.com/documents/8.2.0/api/twisted.web.resource.Resource.html#getChild
- # Если не написать такой код, то пройдя по ссылке с хешем мы получим 404
- # т.к. обработчика на случайный хеш у нас не стоит.
- # пытаемся найти дочерние ресурсы
- child = resource.Resource.getChild(self, name, request)
- if isinstance(child, resource.NoResource):
- return self # если нету - возвращаем текущий ресурс
- else:
- return child
- # Можно сделать просто render(self, request), которая будет отвечать
- # на все запросы.
- # Можно разделить на render_HEAD, GET, POST, PUT, DELETE
- def render_GET(self, request):
- # Поставим нужные заголовки
- request.setHeader("Content-Type", "text/html; charset=utf8")
- # Если обратились к корню, то просто возвращаем форму
- if request.path == "/":
- # Простейший вариант - возвратить строку
- # Twisted отправит её клиенту
- return html % """
- Отличие от обычной сокращалки в том, что она
- проверяет существование чего-либо по ссылке.
- <form method="GET" action="/make/">
- <label for="query">Напишите сюда что-нибудь: </label>
- <input type="text" name="query" />
- <input type="submit" value="YARRR!" />
- </form>"""
- # иначе забираем хеш-тег из пути, ищем ссылку и редиректим на нее
- else:
- hashkey = request.path[1:]
- d = None
- try:
- # d - тот самый Deferred
- # объект, который говорит о том, что его действие будет совершено потом
- d = get_link_by_hash(hashkey)
- # Когда действие d будет готово, он запустит redirect(результат, request)
- d.addCallback(redirect, request)
- except TypeError:
- # Если вместо хеша нам подсунули невесть что, то выдаем 404
- request.setResponseCode(404)
- # Если простейший вариант нас не устраивает - используем
- # request.write и request.finish
- # Установку заголовков "простым вариантом" сделать нельзя
- request.finish()
- # Если в Python-функции отсутствует return, то
- # по умолчанию функция возвращает None
- # Впрочем, нам уже не важно - мы закрыли соединение
- else:
- # NOT_DONE_YET - значит полностью страничка пользователю
- # будет выдана когда-нибудь потом.
- # Очень важно в случае возникновения ошибки все равно
- # сделать request.finish(), иначе браузер будет долго ждать
- # своего результата.
- return server.NOT_DONE_YET
- # Обработчик создания ссылки я вынес отдельно
- # В принципе, можно было руками парсить request.path и делать
- # всё в одном обработчике. Но это будет PHP какой-то тогда.
- class LinkMaker(resource.Resource):
- isLeaf = True # У этого ресурса не будет дочерних ресурсов
- def render_GET(self, request):
- request.setHeader("Content-Type", "text/html; charset=utf8")
- query = None
- try:
- query = request.args["query"][0]
- if len(query) == 0:
- raise KeyError()
- except KeyError:
- return html % "А на что ссылаться-то? Вернитесь и напишите ссылку."
- else:
- # несколько строчек для любителей интернациональных доменов
- import urlparse
- url = list(urlparse.urlsplit(query, scheme="http"))
- url[1] = unicode(url[1], "utf-8").encode("idna") # просто перекодируем домен...
- query = urlparse.urlunsplit(tuple(url)).replace(":///", "://") # небольшой баг urlparse
- check_link(query, request)
- return server.NOT_DONE_YET
- # Эту функцию мы будем везде вешать как обработчик ошибок
- # Если в процессе работы какого-то Deferred произойдет ошибка
- # То он вызовет свои errback
- # В аргументах got_error есть второй аргумент request, который нужно
- # указывать в addErrback
- def got_error(failure, request):
- # Если это ошибка соединения - сообщим пользователю
- # Ошибки, генерируемые Twisted, содержат поле value
- if (hasattr(failure, "value")):
- try:
- f = failure.value[0]
- except (KeyError, TypeError):
- f = failure.value
- request.write(error_html % f)
- request.finish()
- # Если это моя ошибка - сообщим трейсбаком в лог
- else:
- request.setResponseCode(500)
- request.finish()
- from traceback import print_exc
- print_exc()
- # Проверка существования ссылки
- def check_link(link, request):
- agent = client.Agent(reactor)
- # Сначала попытаемся проверить ссылку по HEAD
- # d - тот самый Deferred
- # объект, который говорит о том, что его действие будет совершено потом
- d = agent.request("HEAD", link)
- d.addErrback(got_error, request)
- # Указываем, какое действие должен будет сделать d.
- # ВНИМАНИЕ! got_result будет ссылкой на d, а не функцией!
- # Эту функцию мы больше нигде использовать не будем,
- # поэтому спокойно вешаем декоратор.
- # Вообще я не видел, чтобы кто-то еще так делал ☺
- # но если оно выглядит красивее, то почему бы и нет?
- # Минус в том, что каждый раз при вызове будет создаваться
- # новый объект-функция, а это немножко замедлит наше приложение.
- @d.addCallback
- def check_error(result):
- if request.code >= 400:
- # Не получился HEAD? Может удаленный сервер просто не поддерживает
- # Пробуем GET
- d = agent.request("GET", link)
- d.addErrback(got_error, request)
- # Почему неправильный результат будет направлен в callback,
- # а не в errback?
- # Неправильный результат - тоже результат.
- # errback будет вызван только если результата нет вообще.
- @d.addCallback
- def check_error_again(result):
- if request.code >= 400:
- request.write(error_html % (
- "Сервер вернул ошибку %s" % failure.code))
- request.finish()
- # Обратите внимание, мы добавляем к d второй callback
- # Все callback будут вызываться по очереди, в том порядке,
- # в котором они были добавлены.
- # Кстати, если один из callback-ов что-то возвращает,
- # то это "что-то" пойдет как первый аргумент в следующий callback.
- d.addCallback(got_result, link, request)
- else:
- # got_result - обычная функция. Можно вызывать как есть,
- # можно пихать в callback
- got_result(result, link, request)
- return d
- # Будет вызвано тогда, когда удаленный сервер ответит положительным результатом
- # то есть указанная пользователем ссылка существует и работает
- def got_result(result, link, request):
- # Для примера я решил взять PostgreSQL. MySQL не умеет INSERT...RETURNING
- # Как вы уже заметили - любое откладываемое действие требует отдельной функции
- # Поэтому еще один запрос в БД добавит еще 5-15 строк кода.
- db = adbapi.ConnectionPool("psycopg2", **db_options)
- # Кстати. AFAIK драйвер psycopg2 сам по себе является синхронным,
- # поэтому Twisted оборачивает все его запросы в Python-потоки (threads),
- # так что в силу природы потоков в Python
- # такой драйвер будет работать медленнее
- d = db.runQuery(
- "INSERT INTO urls (link) VALUES ('%s') RETURNING id" % adbapi.safe(link))
- d.addCallback(return_link_to_user, request)
- d.addErrback(got_error, request)
- # Здесь - логика вычисления случайных буков для сокращенной ссылки. Она тривиальна.
- # Маленькая функция, которая преобразует integer из базы данных
- # в питоновскую бинарную строку, которую мы уже потом обернем в base64
- def int_to_string(i):
- if i > 0:
- return chr(i % 256) + int_to_string(i / 256)
- else:
- return ""
- # Действие наоборот - бинарную строку мы конвертируем в integer
- def string_to_int(s):
- return reduce(lambda x, y: x * 256 + y, (ord(i) for i in s[::-1]), 0)
- # База данных успешно отработала, покажем пользователю полученную ссылку
- def return_link_to_user(result, request):
- hashkey = None
- # Маловероятно, но БД может не вернуть ничего. Подстрахуемся...
- try:
- hashkey = base64.urlsafe_b64encode(int_to_string(result[0][0]))
- except KeyError:
- request.setResponseCode(500)
- request.finish()
- from traceback import print_exc
- print_exc()
- else:
- link = "http://%s/%s" % (request.received_headers['host'], hashkey)
- request.write(html % """Ваша ссылка сокращена. Забирайте её
- <a href="%s">%s</a>""" % (link, link))
- request.finish()
- # Превращаем хеш-код ссылки в integer
- # который посмотрим в нашей БД
- def get_link_by_hash(hashkey):
- number = string_to_int(base64.urlsafe_b64decode(hashkey))
- db = adbapi.ConnectionPool("psycopg2", **db_options)
- d = db.runQuery(
- "SELECT link FROM urls WHERE id = %s" % int(number))
- return d
- def redirect(link, request):
- # Пустой результат - тоже результат
- # Если ссылки по заданному идентификатору нет, то link будет пустой
- try:
- request.redirect(link[0][0])
- except IndexError:
- # По идее не помешало бы 404, но chromium не показывает такие странички
- request.setResponseCode(200)
- request.write(error_html % "Такую ссылку еще не придумали.")
- finally:
- request.finish()
- # Это показ исходного кода. Можно не вчитываться
- try:
- from pygments import highlight, formatters, lexers
- from os import path
- lexer = lexers.get_lexer_by_name("python", encoding="utf-8")
- formatter = formatters.get_formatter_by_name("html", noclasses=True, linenos="table")
- source_code_string = highlight(
- open(path.abspath(__file__)).read(),
- lexer, formatter).encode("utf-8")
- except:
- source_code_string = "Ой ☹ Исходники недоступны."
- class SourceCodeResource(resource.Resource):
- isLeaf = True
- def render_GET(self, request):
- request.setHeader("Content-Type", "text/html; charset=utf8")
- return html % source_code_string
- # Конец показа исходников
- if __name__ == "__main__":
- # Сначала попытаемся взять номер порта для прослушивания из
- # аргументов командной строки
- from sys import argv
- if len(argv) > 1:
- port = int(argv[1])
- # По умолчанию слушаем на 0.0.0.0:8000
- else:
- port = 8000
- # Отладочная информация в stdout
- log.startLogging(log.sys.stdout)
- # Создаем наш ресурс. Он будет принимать все, что начинается на /, кроме...
- index = Index()
- # ... кроме /make/, который будет обрабатывать другой ресурс ...
- index.putChild("make", LinkMaker())
- # ... и /code/, который показывает исходный код этого файла.
- index.putChild("source", SourceCodeResource())
- # Прослушиваем порт...
- site = server.Site(index)
- reactor.listenTCP(port, site)
- # Самое главное! Запускаем цикл событий Twisted, тот самый механизм,
- # который когда надо запускает callback-и у Deferred-ов (и не только).
- # Таким образом, наша программа будет работать до тех пор,
- # пока мы не вызовем reactor.stop()
- # (то есть этот пример будет работать пока программу не убьют из ОС)
- reactor.run()
Add Comment
Please, Sign In to add comment