Son propre TinyURL en Python et HTML5 avec webpy

vignette

Avec Twitter, la concision est de mise. Tout le monde utilise des "raccourcisseurs" d'URL comme TinyURL ou Bit.ly mais ça pose plusieurs problèmes : vous n'avez aucune idée de la pérennité du service (et en ce moment on voit bien le problème des services gratuits et non rentables qui ferment) et aucune garantie que les liens seront toujours redirigés vers les bonnes destinations sans passer par une pub/un outil de traçabilité/insérez votre délire parano ici.

J'ai enregistré hier bgk.me pour remédier à ça et avoir mon propre service de redirections courtes. Ça prend une centaine de lignes en Python et c'est sous WTFPL, comme ce blog. Enfin HTML5 c'est juste pour être plus concis, rien de bien évolué dans ce domaine, allez voir l'excellent billet de Maurice si vous voulez apprendre à exploiter certaines capacités utiles de HTML5.

Choix techniques

Au niveau des fonctionnalités :

  • la possibilité de rediriger facilement vers mes billets ou mes brèves ;
  • la possibilité de choisir un nom de raccourci pertinent (je déteste les tinyurls qui ont un hash ne permettant pas d'avoir une idée de ce qu'il y a derrière) ;
  • une interface d'administration simpliste.

Il y a des centaines de façons de coder ça et j'aurais aussi pu utiliser ur1 en PHP mentionné par znarf, je pense que c'est une bonne solution aussi, tout dépend de votre infrastructure. Edit : Xavier vient de mentionner urly en Python aussi.

Je voulais rester super simple, un seul fichier, pas de base de données et une base qui ne soit pas une usine à gaz comme Django, inutile dans notre cas. J'ai donc opté pour webpy qui est excellent pour ça, bon c'est devenu un peu trop complet à mon goût encore (ah le bon temps où ça tenait dans un fichier unique :-)) mais suffisamment simple à prendre en main pour ceux qui voudraient se mettre à Python ;-).

Redirections

On commence par les redirections des billets et brèves, il faut pour cela ajouter les URL suivantes :

urls = (
    "/p/(\d+)",         "RedirectToPost",
    "/t/(\d+)",         "RedirectToThought",
)

Et ensuite construire les classes de redirection :

class RedirectToPost:
    def GET(self, post_id):
        return web.redirect(POST_REDIRECT_URL % post_id)

class RedirectToThought:
    def GET(self, thought_id):
        return web.redirect(THOUGHT_REDIRECT_URL % thought_id)

Ici vous pouvez voir que j'ai misé sur la réutilisation du code en définissant pas mal de variables en début de fichier permettant d'adapter facilement le code à votre propre usage. Grâce à ces redirections, http://bgk.me/p/270 va rediriger vers ce billet par exemple.

Passons maintenant au redirections vers d'autres URL, ici aussi il faut ajouter une entrée dans les URL :

urls = (
    "/(.*)",            "RedirectToOthers",
)

Là il faut aller chercher les URL qui ont été créées via l'admin (que l'on verra ensuite) et qui sont stockées dans un objet shelve qui permet rapidement d'avoir une correspondance clé-valeur en Python.

class RedirectToOthers:
    def GET(self, short_name):
        storage = shelve.open(SHELVE_FILENAME)
        # shelve does not allow unicode keys
        short_name = str(short_name)
        if storage.has_key(short_name):
            response = web.redirect(storage[short_name])
        else:
            response = FAIL_MESSAGE
        storage.close()
        return response

Ici aussi ça reste très simple puisque ça redirige vers l'URL trouvée ou ça affiche un message d'erreur.

Administration

Bon si vous avez toujours un shell ouvert sur votre serveur, vous pouvez directement remplir votre fichier shelve avec le shell Python. Mais ça coûte pas grand chose de faire une admin toute simple pour pouvoir faire ça en web alors on ne va pas s'en priver.

Pour protéger cette URL, on va juste la rendre difficile à trouver, je vous laisser utiliser ce qui vous semble le plus pertinent, ici aussi dans une variable :

urls = (
    ADMIN,              "Admin",
    ADMIN+"/done/(.*)", "AdminDone",
)

La classe Admin vous permet d'afficher le formulaire et de soumettre une nouvelle correspondance raccourci-url :

class Admin:
    def GET(self):
        admin_form = web.form.Form(
            web.form.Textbox("url",     description="Long URL"),
            web.form.Textbox("shortcut",description="Shortcut"),
        )
        admin_template = web.template.Template("""$def with(form)
        <!DOCTYPE HTML>
        <html lang="en">
          <head>
            <meta charset=utf-8>
            <title>URL shortener administration</title>
          </head>
          <body onload="document.getElementById('url').focus()">
            <header><h1>Admin</h1></header>
            <form method="POST" action="/admin">
              $:form.render()
              <input type="submit" value="Shorten this long URL">
            </form>
          </body>
        </html>
        """)
        return admin_template(admin_form())

    def POST(self):
        data = web.input()
        shortcut = str(data.shortcut) or random_shortcut()
        storage = shelve.open(SHELVE_FILENAME)
        if storage.has_key(shortcut) or not data.url:
            response = web.badrequest()
        else:
            storage[shortcut] = data.url
            response = web.seeother(ADMIN+'/done/'+shortcut)
        storage.close()
        return response

Les formulaires et templates de webpy sont utilisés directement dans le code ici car ils restent super concis. Si c'est un GET on construit le formulaire et on l'envoie au template, si c'est un POST on ajoute l'URL à la base et on redirige vers la page de confirmation. Très peu de vérifications car ça ne sert pas à grand chose dans ce cas, on s'assure juste de ne pas écraser une URL existante et qu'une URL a bien été soumise.

Il ne reste plus qu'à afficher une page la page de confirmation avec le lien nouvellement créer (en dur sinon on pourrait facilement détecter votre admin grâce au referer...) et un raccourci pour tweeter le lien directement :

class AdminDone:
    def GET(self, short_name):
        admin_done_template = web.template.Template("""$def with(new_url)
        <!DOCTYPE HTML>
        <html lang="en">
          <head>
            <meta charset=utf-8>
            <title>URL shortener administration</title>
          </head>
          <body>
            <header><h1>Done!</h1></header>
            <p>You created: $new_url</p>
            <p><a href="http://twitter.com/home?status=$new_url" 
              title="Tweet it!">Tweet it?</a></p>
          </body>
        </html>
        """)
        return admin_done_template(SERVICE_URL+short_name)

Et voilà, après vous pouvez ajouter tout pleins de choses mais la base est là, suffisante pour mon usage. Ça m'a pris 2h et 10€ (car ils se gavent sur les .me mais c'est la seule extension qu'il restait) mais c'est le prix de l'indépendance.

Mise en production

J'utilise lighty, à adapter selon votre configuration (ne pas oublier de rendre code.py exécutable et de modifier ADMIN !) :

$HTTP["host"] =~ "bgk.me" {
        server.document-root = "/path/"

        fastcgi.server = (
            "/code.py" => (
                "main" => (
                    "socket" => "/path/bgkme.socket",
                    "bin-path" => "/path/code.py",
                    "max-procs" => 1,
                    "bin-environment" => (
                        "REAL_SCRIPT_NAME" => ""
                    ),
                    "check-local" => "disable"
                )
            )
        )

        url.rewrite-once = (
            "^(/.*)$" => "/code.py$1",
        )
}

Pour terminer, le code est sur ma ferme de dépôts, à utiliser, modifier, critiquer sans modération, enjoy!

— 21/02/2009

Articles peut-être en rapport

Commentaires

Yoan le 21/02/2009 :

Oho, même toi tu es diabolique : body onload="document.getElementById('url').focus()"

Sinon, juste pour dire qu'HTML 5 est un peu plus qu'un doctype dont il est possible de se souvenir. Tu peux supprimer les balises head et body de ta page et utilise cette splendide balise meta: <meta charset=utf-8> et n'oublie pas l'attribut lang sur ta balise html, ça fait toujours plaisir. Et as-tu jeter un œil à autofocus? C'est toujours aussi diabolique à mon sens, mais il est possible que le navigateur gère ça de manière plus agréable.

Sinon, je découvre les web.form.Form et ai déjà des frissons dans le dos.

Damien B le 21/02/2009 :

"Enfin HTML5 c'est juste pour être plus concis"

Tu veux dire, HTML 5 peut-être ?

"<meta http-equiv="Content-Type" content="text/html; UTF-8" />"

validator.nu dit : "Error: Bad value text/html; UTF-8 for attribute content on element meta: The legacy encoding did not contain charset= immediately after the space.

From line 4, column 13; to line 4, column 73"

Ralalalala : trop concis.

"<meta http-equiv="Content-Type" content="text/html; UTF-8" />"

§8.1.2.1 point 6 dit très clairement que /> est tout à fait optionnel, tu aurais donc pu finir par 'UTF-8">' : deux caractères de gagnés pour la concision. Et pareil pour les input.

"<header><h1>Admin</h1></header>"

Vu la taille et le contenu du document, header est totalement inutile : 17 caractères de gagnés.

Au final, avec tout ce qu'on gagne, tu peux te permettre de mettre
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
sans perdre un caractère de concision, et tu es valide par rapport aux spécifications publiées :) Bon, c'est vrai qu'au passage on perd un effet de manche : c'est dommage :-P

David, biologeek le 22/02/2009 :

Ok, bon je vais être honnête j'ai fait un vilain copier-coller de l'en-tête de l'ami mauriz et j'aurais pas dû, surtout que j'ai été assez faignant pour pas valider le code ensuite... bref, mea culpa, je vais corriger ça.

@Yoan : par contre autofocus, tu as un lien pour ça ?

Damien B le 22/02/2009 :

Tiens, pour autofocus : http://dev.w3.org/html5/spec/Overview.html#autofocusing-a-form-control

Et tu feras attention sur ton blog, ton colorisateur syntaxique n'est pas assez pragmatique pour détecter "header". Mwahahaha.

David, biologeek le 22/02/2009 :

Merci Damien, bon du coup je vais plus pouvoir utiliser les forms de webpy, ce qui ne va pas non plus trop me manquer.

Et sinon oui, je viens de mettre à jour le highlight.js :p

Damien B le 22/02/2009 :

Sinon, pas terrible d'avoir fait sauter le titre du post du fil de commentaires, on ne sait plus à quelle conversation ça appartient.

David, biologeek le 22/02/2009 :

Bien vu, et corrigé. C'est dû à ma récente utilisation de la solution native de Django pour les commentaires. Merci pour le report de bug :)

Yoan le 22/02/2009 :

L'ami Lachlan (http://standardssuck.org/) a écrit une série de tests concernant l'autofocus : http://lachy.id.au/dev/markup/tests/html5/autofocus/

Oui, le premier doctest des forms web.py donne des boutons. Web.py contient trop d'outils dont on a pas envie de se servir à mon goût.

>>> import web
>>> web.utils.commify(1234567890)
'1,234,567,890'
>>> web.utils.nthstr(1001)
'1001st'

Certaines choses doivent rester hors d'un web framework dont le motto est d'être simple et puissant. Je trouve Werkzeug séduisant, même si je passe pas mal de temps à jouer avec Restish. Enfin, bref, les goûts et les couleurs comme on dit.

sebsauvage le 22/02/2009 :

web.py est pas mal, oui.
Mais comme toi, parfois je cherche encore plus minimal. J'ai fini par écrire le mien (pour ce que ça vaut):

http://sebsauvage.net/python/snyppets/#simplewebdispatcher

Amusant à faire, et vraiment minimaliste.

Un petit coup de l'excellent SQLite là dessus et des cookies , et on a ce qu'il faut pour gérer les sessions.

NiCoS le 22/02/2009 :

Si tu devais faire un petit comparatif web.py / django, tu en dirais quoi ?

J'avias vu web.py il y a qqs temps mais pour éviter de me disperser, je l'avais laissé de coté. Vous avez déjà tous l'air de lui reprocher le fait qu'il ne soit plus aussi léger qu'auparavant...

Pierre-Jean le 23/02/2009 :

> NiCoS, A l'époque où je l'avais regardé, le code source de web.py comportait beaucoup de "magic".

Mais suite à cet article je vais refaire un petit tour d'évaluation.

David, biologeek le 23/02/2009 :

@Yoan : merci pour les liens, bon la conclusion c'est que ça ne fonctionne pas encore dans mes versions de Firefox ou Safari...

C'est marrant car j'ai failli le faire avec werkzeug, je vais jeter un œil à restish.

@sebsauvage : au bout d'un moment c'est fatigant (mais intéressant...) de tout refaire à la main ;-)

@NiCoS :

> Si tu devais faire un petit comparatif web.py / django, tu en dirais quoi ?

Pas les mêmes usages/objectifs, tout dépend des besoins du projet, comme toujours.

@Pierre-Jean : au plus tu simplifies et au plus ça devient magique, forcément (à moins de créer un langage qui soit vraiment orienté web ?), le tout est de trouver le bon équilibre !

NiCoS le 23/02/2009 :

En fait, oups, j'ai confusionné avec web2py (http://mdp.cti.depaul.edu/) qui semble plus proche de django que web.py ;-)

Après lecture du site, en effet, rien à voir !

desfrenes le 10/03/2009 :

je crois aussi qu'une comparaison djang/web2py un minimum objective (cad pas faite par l'auteur de web2py!) serait intéressante.

David, biologeek le 12/03/2009 :

Les comparaisons/benchmarks objectifs sont assez rares :)

desfrenes le 15/03/2009 :

Y compris ton article dans linux mag? ;-)

David, biologeek le 16/03/2009 :

Héhé, surtout celui-là ! :-)

neofutur le 19/08/2011 :

dns le genre alternatives en php, voir aussi casimir :

https://github.com/nhoizey/casimir

et mon fork avec 1 ou 2 features en plus, et quelques fixes pour le php 5.3 :
https://github.com/neofutur/casimir