Guest User

Untitled

a guest
Mar 12th, 2011
257
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.90 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. import base64
  3.  
  4. from twisted.web import server, resource, client
  5. from twisted.internet import reactor, error
  6. from twisted.python import log
  7. from twisted.enterprise import adbapi
  8.  
  9.  
  10. # Настройки для соединения с БД я вынес отдельно
  11. db_options = {
  12.     "database": "werehuman"
  13. }
  14.  
  15. # Нам нужна всего одна табличка
  16. # CREATE TABLE urls (
  17. #    id SERIAL PRIMARY KEY,
  18. #    link VARCHAR(512) NOT NULL);
  19.  
  20. # Никто не запрещает использовать шаблонизатор
  21. # Но для примера это будет жирновато
  22. html = """<!DOCTYPE html>
  23. <html>
  24. <head><title>Twisted URL Shortener</title></head>
  25. <body style="padding: 50px">
  26. <div style="font-size: 2em; margin-bottom: 2em">Простая сокращалка ссылок</div>
  27. <div>
  28. %s
  29. </div>
  30. <div style="margin-top: 10em"><a href="/">Вернуться на глагне</a><br /><a href="/source/">Залезть под капот</a></div>
  31. </body>
  32. </html>"""
  33.  
  34. error_html = html % "Ой! ☹ Произошла ошибка: %s"
  35.  
  36. # Ресурс, который будет выступать в качестве главной страницы
  37. # Он же будет редиректить на ссылки
  38. class Index(resource.Resource):
  39.     isLeaf = False # Означает, что к этому ресурсу будут подсоединены еще ресурсы (см. ниже)
  40.    
  41.     def getChild(self, name, request):
  42.         # http://twistedmatrix.com/documents/8.2.0/api/twisted.web.resource.Resource.html#getChild
  43.         # Если не написать такой код, то пройдя по ссылке с хешем мы получим 404
  44.         # т.к. обработчика на случайный хеш у нас не стоит.
  45.        
  46.         # пытаемся найти дочерние ресурсы
  47.         child = resource.Resource.getChild(self, name, request)
  48.         if isinstance(child, resource.NoResource):
  49.             return self # если нету - возвращаем текущий ресурс
  50.         else:
  51.             return child
  52.    
  53.     # Можно сделать просто render(self, request), которая будет отвечать
  54.     # на все запросы.
  55.     # Можно разделить на render_HEAD, GET, POST, PUT, DELETE
  56.     def render_GET(self, request):
  57.         # Поставим нужные заголовки
  58.         request.setHeader("Content-Type", "text/html; charset=utf8")
  59.        
  60.         # Если обратились к корню, то просто возвращаем форму
  61.         if request.path == "/":
  62.             # Простейший вариант - возвратить строку
  63.             # Twisted отправит её клиенту
  64.             return html % """
  65.                Отличие от обычной сокращалки в том, что она
  66.                проверяет существование чего-либо по ссылке.
  67.                <form method="GET" action="/make/">
  68.                    <label for="query">Напишите сюда что-нибудь: </label>
  69.                    <input type="text" name="query" />
  70.                    <input type="submit" value="YARRR!" />
  71.                </form>"""
  72.         # иначе забираем хеш-тег из пути, ищем ссылку и редиректим на нее
  73.         else:
  74.             hashkey = request.path[1:]
  75.             d = None
  76.             try:
  77.                 # d - тот самый Deferred
  78.                 # объект, который говорит о том, что его действие будет совершено потом
  79.                 d = get_link_by_hash(hashkey)
  80.                 # Когда действие d будет готово, он запустит redirect(результат, request)
  81.                 d.addCallback(redirect, request)
  82.             except TypeError:
  83.                 # Если вместо хеша нам подсунули невесть что, то выдаем 404
  84.                 request.setResponseCode(404)
  85.                 # Если простейший вариант нас не устраивает - используем
  86.                 # request.write и request.finish
  87.                 # Установку заголовков "простым вариантом" сделать нельзя
  88.                 request.finish()
  89.                 # Если в Python-функции отсутствует return, то
  90.                 # по умолчанию функция возвращает None
  91.                 # Впрочем, нам уже не важно - мы закрыли соединение
  92.             else:
  93.                 # NOT_DONE_YET - значит полностью страничка пользователю
  94.                 # будет выдана когда-нибудь потом.
  95.                 # Очень важно в случае возникновения ошибки все равно
  96.                 # сделать request.finish(), иначе браузер будет долго ждать
  97.                 # своего результата.
  98.                 return server.NOT_DONE_YET
  99.        
  100. # Обработчик создания ссылки я вынес отдельно
  101. # В принципе, можно было руками парсить request.path и делать
  102. # всё в одном обработчике. Но это будет PHP какой-то тогда.
  103. class LinkMaker(resource.Resource):
  104.     isLeaf = True # У этого ресурса не будет дочерних ресурсов
  105.    
  106.     def render_GET(self, request):
  107.         request.setHeader("Content-Type", "text/html; charset=utf8")
  108.         query = None
  109.         try:
  110.             query = request.args["query"][0]
  111.             if len(query) == 0:
  112.                 raise KeyError()
  113.         except KeyError:
  114.             return html % "А на что ссылаться-то? Вернитесь и напишите ссылку."
  115.         else:
  116.             # несколько строчек для любителей интернациональных доменов
  117.             import urlparse
  118.             url = list(urlparse.urlsplit(query, scheme="http"))
  119.             url[1] = unicode(url[1], "utf-8").encode("idna") # просто перекодируем домен...
  120.             query = urlparse.urlunsplit(tuple(url)).replace(":///", "://") # небольшой баг urlparse
  121.            
  122.             check_link(query, request)
  123.             return server.NOT_DONE_YET
  124.            
  125.  
  126. # Эту функцию мы будем везде вешать как обработчик ошибок
  127. # Если в процессе работы какого-то Deferred произойдет ошибка
  128. # То он вызовет свои errback
  129. # В аргументах got_error есть второй аргумент request, который нужно
  130. # указывать в addErrback
  131. def got_error(failure, request):
  132.     # Если это ошибка соединения - сообщим пользователю
  133.     # Ошибки, генерируемые Twisted, содержат поле value
  134.     if (hasattr(failure, "value")):
  135.         try:
  136.             f = failure.value[0]
  137.         except (KeyError, TypeError):
  138.             f = failure.value
  139.         request.write(error_html % f)
  140.         request.finish()
  141.     # Если это моя ошибка - сообщим трейсбаком в лог
  142.     else:
  143.         request.setResponseCode(500)
  144.         request.finish()
  145.         from traceback import print_exc
  146.         print_exc()
  147.  
  148.  
  149. # Проверка существования ссылки
  150. def check_link(link, request):
  151.     agent = client.Agent(reactor)
  152.    
  153.     # Сначала попытаемся проверить ссылку по HEAD
  154.    
  155.     # d - тот самый Deferred
  156.     # объект, который говорит о том, что его действие будет совершено потом
  157.     d = agent.request("HEAD", link)
  158.     d.addErrback(got_error, request)
  159.    
  160.     # Указываем, какое действие должен будет сделать d.
  161.     # ВНИМАНИЕ! got_result будет ссылкой на d, а не функцией!
  162.     # Эту функцию мы больше нигде использовать не будем,
  163.     # поэтому спокойно вешаем декоратор.
  164.     # Вообще я не видел, чтобы кто-то еще так делал ☺
  165.     # но если оно выглядит красивее, то почему бы и нет?
  166.     # Минус в том, что каждый раз при вызове будет создаваться
  167.     # новый объект-функция, а это немножко замедлит наше приложение.
  168.     @d.addCallback
  169.     def check_error(result):
  170.         if request.code >= 400:
  171.             # Не получился HEAD? Может удаленный сервер просто не поддерживает
  172.             # Пробуем GET
  173.             d = agent.request("GET", link)
  174.             d.addErrback(got_error, request)
  175.            
  176.             # Почему неправильный результат будет направлен в callback,
  177.             # а не в errback?
  178.             # Неправильный результат - тоже результат.
  179.             # errback будет вызван только если результата нет вообще.
  180.             @d.addCallback
  181.             def check_error_again(result):
  182.                 if request.code >= 400:
  183.                     request.write(error_html % (
  184.                         "Сервер вернул ошибку %s" % failure.code))
  185.                     request.finish()
  186.              
  187.             # Обратите внимание, мы добавляем к d второй callback
  188.             # Все callback будут вызываться по очереди, в том порядке,
  189.             # в котором они были добавлены.
  190.             # Кстати, если один из callback-ов что-то возвращает,
  191.             # то это "что-то" пойдет как первый аргумент в следующий callback.
  192.             d.addCallback(got_result, link, request)
  193.         else:
  194.             # got_result - обычная функция. Можно вызывать как есть,
  195.             # можно пихать в callback
  196.             got_result(result, link, request)
  197.        
  198.     return d
  199.        
  200. # Будет вызвано тогда, когда удаленный сервер ответит положительным результатом
  201. # то есть указанная пользователем ссылка существует и работает
  202. def got_result(result, link, request):
  203.     # Для примера я решил взять PostgreSQL. MySQL не умеет INSERT...RETURNING
  204.     # Как вы уже заметили - любое откладываемое действие требует отдельной функции
  205.     # Поэтому еще один запрос в БД добавит еще 5-15 строк кода.
  206.     db = adbapi.ConnectionPool("psycopg2", **db_options)
  207.     # Кстати. AFAIK драйвер psycopg2 сам по себе является синхронным,
  208.     # поэтому Twisted оборачивает все его запросы в Python-потоки (threads),
  209.     # так что в силу природы потоков в Python
  210.     # такой драйвер будет работать медленнее
  211.     d = db.runQuery(
  212.         "INSERT INTO urls (link) VALUES ('%s') RETURNING id" % adbapi.safe(link))
  213.     d.addCallback(return_link_to_user, request)
  214.     d.addErrback(got_error, request)
  215.    
  216.  
  217. # Здесь - логика вычисления случайных буков для сокращенной ссылки. Она тривиальна.
  218. # Маленькая функция, которая преобразует integer из базы данных
  219. # в питоновскую бинарную строку, которую мы уже потом обернем в base64
  220. def int_to_string(i):
  221.     if i > 0:
  222.         return chr(i % 256) + int_to_string(i / 256)
  223.     else:
  224.         return ""
  225.  
  226. # Действие наоборот - бинарную строку мы конвертируем в integer
  227. def string_to_int(s):
  228.     return reduce(lambda x, y: x * 256 + y, (ord(i) for i in s[::-1]), 0)
  229.    
  230.    
  231. # База данных успешно отработала, покажем пользователю полученную ссылку
  232. def return_link_to_user(result, request):
  233.     hashkey = None
  234.     # Маловероятно, но БД может не вернуть ничего. Подстрахуемся...
  235.     try:
  236.         hashkey = base64.urlsafe_b64encode(int_to_string(result[0][0]))
  237.     except KeyError:
  238.         request.setResponseCode(500)
  239.         request.finish()
  240.         from traceback import print_exc
  241.         print_exc()
  242.     else:
  243.         link = "http://%s/%s" % (request.received_headers['host'], hashkey)
  244.         request.write(html % """Ваша ссылка сокращена. Забирайте её
  245.                      <a href="%s">%s</a>""" % (link, link))
  246.         request.finish()
  247.  
  248.  
  249. # Превращаем хеш-код ссылки в integer
  250. # который посмотрим в нашей БД
  251. def get_link_by_hash(hashkey):
  252.     number = string_to_int(base64.urlsafe_b64decode(hashkey))
  253.     db = adbapi.ConnectionPool("psycopg2", **db_options)
  254.     d = db.runQuery(
  255.         "SELECT link FROM urls WHERE id = %s" % int(number))
  256.     return d
  257.    
  258. def redirect(link, request):
  259.     # Пустой результат - тоже результат
  260.     # Если ссылки по заданному идентификатору нет, то link будет пустой
  261.     try:
  262.         request.redirect(link[0][0])
  263.     except IndexError:
  264.         # По идее не помешало бы 404, но chromium не показывает такие странички
  265.         request.setResponseCode(200)
  266.         request.write(error_html % "Такую ссылку еще не придумали.")
  267.     finally:
  268.         request.finish()
  269.        
  270.        
  271. # Это показ исходного кода. Можно не вчитываться
  272. try:
  273.     from pygments import highlight, formatters, lexers
  274.     from os import path
  275.     lexer = lexers.get_lexer_by_name("python", encoding="utf-8")
  276.     formatter = formatters.get_formatter_by_name("html", noclasses=True, linenos="table")
  277.     source_code_string = highlight(
  278.         open(path.abspath(__file__)).read(),
  279.         lexer, formatter).encode("utf-8")
  280. except:
  281.     source_code_string = "Ой ☹ Исходники недоступны."
  282.  
  283. class SourceCodeResource(resource.Resource):
  284.     isLeaf = True
  285.    
  286.     def render_GET(self, request):
  287.         request.setHeader("Content-Type", "text/html; charset=utf8")
  288.         return html % source_code_string
  289.        
  290. # Конец показа исходников
  291.  
  292.  
  293.        
  294. if __name__ == "__main__":
  295.     # Сначала попытаемся взять номер порта для прослушивания из
  296.     # аргументов командной строки
  297.     from sys import argv
  298.     if len(argv) > 1:
  299.         port = int(argv[1])
  300.     # По умолчанию слушаем на 0.0.0.0:8000
  301.     else:
  302.         port = 8000
  303.        
  304.     # Отладочная информация в stdout
  305.     log.startLogging(log.sys.stdout)
  306.    
  307.     # Создаем наш ресурс. Он будет принимать все, что начинается на /, кроме...
  308.     index = Index()
  309.     # ... кроме /make/, который будет обрабатывать другой ресурс ...
  310.     index.putChild("make", LinkMaker())
  311.     # ... и /code/, который показывает исходный код этого файла.
  312.     index.putChild("source", SourceCodeResource())
  313.    
  314.     # Прослушиваем порт...
  315.     site = server.Site(index)
  316.     reactor.listenTCP(port, site)
  317.    
  318.     # Самое главное! Запускаем цикл событий Twisted, тот самый механизм,
  319.     # который когда надо запускает callback-и у Deferred-ов (и не только).
  320.     # Таким образом, наша программа будет работать до тех пор,
  321.     # пока мы не вызовем reactor.stop()
  322.     # (то есть этот пример будет работать пока программу не убьют из ОС)
  323.     reactor.run()
Add Comment
Please, Sign In to add comment