title:      Son propre TinyURL en Python et HTML5 avec webpy
slug:       son-propre-tinyurl-en-python-et-html5-avec-webpy
date:       2009-02-21 20:55:06
type:       post
vignette:   images/logos/bitly.png
contextual_title1:  Mieux communiquer sur OpenID et OAuth
contextual_url1:    20090125-mieux-communiquer-sur-openid-et-oauth
contextual_title2:  Sortie de Django 1.0, une année de nouveautés
contextual_url2:    20080902-sortie-de-django-10-une-annee-de-nouveautes
contextual_title3:  ★ iPheeds.org, une version iPhone pour votre blog
contextual_url3:    20080723-ipheedsorg-une-version-iphone-pour-votre-blog

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](http://sam.zoy.org/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](http://svay.com/blog/index/post/2009/02/19/Creer-un-client-Twitter-offline-pour-l-iPhone-avec-HTML5) 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](http://ur1.ca/) en PHP [mentionné par znarf](http://twitter.com/znarf/status/1230052100), je pense que c'est une bonne solution aussi, tout dépend de votre infrastructure. **Edit** : [Xavier vient de mentionner urly](http://twitter.com/xavierlacot/status/1232570280) 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](http://webpy.org/) 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](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](http://www.lighttpd.net/), à 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](http://code.welldev.org/bgk/), à utiliser, modifier, critiquer sans modération, enjoy!

