Un coup de boost

ajouté le 07.08.2011 dans Documentation • par SwissTenguCommentaires (4)
Tags: nginx memcached optimisation perl python

Mon blog a eu quelques petits hoquets ces dernières heures - c'est normal.
Je me suis amusé à mettre en place diverses choses, dont memcached.

J'ai aussi pris un peu de temps pour mettre en place la "minification" de mes divers javascripts et CSS, ce qui permet d'accélérer un peu le temps de chargement, et de minimiser la taille des fichiers.

Au final, le site devrait aller un poil plus vite, modulo la durée de vie du cache (quelques heures).

Voici comment j'ai pu faire tout ceci:

memcached


J'ai commencé par vouloir employer le décorateur @beaker_cache intégré à Pylons. Mal m'en a pris, les clefs générées dans le cache sont tout bonnement imbuvables, et je n'ai pas trouvé de moyen de sortir les contenus par nginx (ce qui permet donc de passer outre la lenteur de pylons).
Il m'a donc fallu mettre en place une gestion du cache personnalisée. J'ai créé un nouveau fichier lib/cache.py:
Code: python
#-*- coding:utf-8 -*-
import memcache
class Cache(object):
  def __init__(self, url, timeout=None, prefix=None): 
    self.mc = memcache.Client([url]) 
    self.timeout = timeout or 300 
    self.prefix = prefix 

  def get(self, key): 
    return self.mc.get(key) 

  def set(self, key, value, timeout=None): 
    if self.prefix: 
      key = self.prefix + key 
    self.mc.set(key, value, timeout or self.timeout) 

  def revoke(self, key):
    self.mc.delete(key)

import decorator
import app_globals
from pylons.configuration import PylonsConfig
def cache(timeout=None):
  config = PylonsConfig()
  g = app_globals.Globals(config)
  def wrapper(func, *args, **kwargs): 
    request = args[0]._py_object.request 
    content = func(*args, **kwargs) 
    key = request.path_qs 
    g.cache.set(key, str(content.encode('utf-8')), timeout) 
    return content 
  return decorator.decorator(wrapper) 


En gros, je crée une classe "Cache" et un décorateur me permettant d'injecter mes contenus dans le cache - en l’occurrence memcached. La clef est prédictible et simple: le chemin appelé pour accéder au contrôleur, avec les arguments etc.

Ensuite j'ai dû instancier la classe dans lib/app_globals.py:
Code: python
from blogger.lib.cache import Cache
class Globals(object):
    def __init__(self, config):
        self.cache = Cache(url='127.0.0.1:11211')


Après, bin y a plus qu'à employer le décorateur dans les contrôleurs:
Code: python
from blogger.lib.cache import cache
class BlogController(BaseController):
  @cache(600)
  def post(self, id):
    # display a post, with comments
    try:
      c.post = Session.query(Content).filter(Content.id == id).one()
    except:
      redirect( url(controller='blog', action='index') )
    c.title = c.post.title
    c.comments = Session.query(Comment).filter(Comment.commentID == id).order_by(Comment.time.asc()).all()
    return render('/blog/%s/post.html' % self.theme)


Easy ;).

Vient ensuite la partie fun:
dans l'action "post", il affiche les commentaires. Et le formulaire pour en ajouter, avec mon fameux captcha. Ooops, lui, il doit être dynamique !

C'est là que le SSI (Server-Side Inclusion) intervient :).

J'ai donc créé un contrôleur appelé "ssi" de manière à savoir à quoi il sert. Dedans, y a les diverses actions utiles à regénérer le contenu réellement dynamique. Dans mes templates mako, j'ai simplement mis le code permettant à nginx de faire les appels en interne:
Code:
<!--# include file="/ssi/formular/${c.post.id}" -->


Et Voilà, le formulaire est à nouveau dynamique ! Top-moumoute quoi.

Maintenant, reste la configuration nginx. Là, je me suis un peu explosé les dents, principalement à cause de memcached, vu qu'au début je ne savais pas quelles étaient les clefs employées. Et je suis tombé sur ce post. Et j'ai vu la Lumière.
De là, tout a été rapide, et j'ai obtenu cette configuration pour nginx:
Code: perl
server {
# ..... blabla etc
  location /ssi {
    proxy_pass        http://localhost:9090/ssi;
    proxy_set_header  Host $host;
    proxy_set_header  SSL yes ;
    add_header X-Memcached "missed";
  }
  location / {
    proxy_set_header  Host $host;
    proxy_set_header  SSL yes ;
    ssi on;
    if ($request_method = POST) { 
      proxy_pass        http://localhost:9090;
    }
    set $memcached_key $uri;
    memcached_pass 127.0.0.1:11211;
    default_type text/html;
    error_page 404 = @pylons;
  }

  location @pylons {
    ssi on;
    proxy_pass        http://localhost:9090;
    proxy_set_header  Host $host;
    proxy_set_header  SSL yes ;
    add_header X-Memcached "missed";
  }
}


Que de l'amour, cette configuration ;).

Bref. Memcached est mis en place, marche... mon serveur ronronne, et tout va bien.

Mini-lui


Vient ensuite la partie "mes css/js ont des espaces vides, des retours à la ligne inutiles etc... Mais si je les vire, je pourrai plus trop les éditer". Que faire ??
Bah simple:
on va planter un peu de Perl dans la config nginx :)

Pour se faire, il faut ajouter le support Perl dans nginx - je vous laisse vous référer à la doc.
Ensuite, dans la partie http, j'ai ajouté ceci:
Code: perl
http {
# blablab...
  perl_modules perl;
  perl_require JSMinify.pm;
  perl_require CSSMinify.pm;
}


et dans mon vhost:
Code: perl
server {
  # blabla..
  location ~* \.css$ {
    root              /var/www/vhosts/blog.tengu.ch/blogger/blogger/public/;
    expires           30d;
    add_header        Cache-Control '30d, must-revalidate';
    try_files $uri @miniCSS;
  }
  location @miniCSS {
    perl CSSMinify::handler;
  }
  location ~* \.js$ {
    root              /var/www/vhosts/blog.tengu.ch/blogger/blogger/public/;
    expires           30d;
    add_header        Cache-Control '30d, must-revalidate';
    try_files $uri @miniJS;
  }
  location @miniJS {
    perl JSMinify::handler;
  }
  # blabla
}


J'vais être gentil et vous filer le code des deux minifiers - il est vraiment simple:
Code: perl
package CSSMinify;
use nginx;
use CSS::Minifier qw(minify);
sub handler {
  my $r=shift;
  my $cache_dir="/tmp"; # Cache directory where minified file will be kept
  my $cache_file=$r->uri;
  $cache_file=~s!/!_!g;
  $cache_file=join("/", $cache_dir, $cache_file);
  my $uri=$r->uri;
  my $filename=$r->filename;
  local $/=undef;
  return DECLINED unless -f $filename;

  if (! -f $cache_file) {
    open(INFILE, $filename) or die "Error reading file: $!";
    open(OUTFILE, '>' . $cache_file ) or die "Error writting file:
    $! $cache_file\n";
    minify(input => *INFILE, outfile => *OUTFILE);
    close(INFILE);
    close(OUTFILE);
  }

  $r->send_http_header('text/css');
  $r->header_out('Cache-Control', '30d, must-revalidate');
  $r->sendfile($cache_file);
  return OK;
}
1;
__END__

Code: perl
package JSMinify;
use nginx;
use JavaScript::Minifier qw(minify);
sub handler {
  my $r=shift;
  my $cache_dir="/tmp"; # Cache directory where minified file will be kept
  my $cache_file=$r->uri;
  $cache_file=~s!/!_!g;
  $cache_file=join("/", $cache_dir, $cache_file);
  my $uri=$r->uri;
  my $filename=$r->filename;
  return DECLINED unless -f $filename;

  if (! -f $cache_file) {
    open(INFILE, $filename) or die "Error reading file: $!";
    open(OUTFILE, '>' . $cache_file ) or die "Error writting file:
    $!";
    minify(input => *INFILE, outfile => *OUTFILE);
    close(INFILE);
    close(OUTFILE);
  }

  $r->header_out('Cache-Control', '30d, must-revalidate');
  $r->send_http_header('text/css');
  $r->sendfile($cache_file);
  return OK;
}
1;
__END__

(Note: je les ai mis dans /etc/perl)

Il me reste encore à intégrer la compression gzip - là j'ai eu pas mal de problèmes et ce n'est pas encore en place.
Il faut savoir que la méthode sendfile() bypass complètement les filtres gzip et charset (et peut-être encore un autre) - donc il faut faire tout cela dans le module perl, en live... et tenter de balancer le bon header, au bon moment, de la bonne manière... Pas encore gagné :(.

Bref. Avec ces 2-3 améliorations, le blog va un poil plus vite, y a moins de trucs qui transitent, mais c'est pas encore optimisé au max.

Il manque encore:
- arriver à faire passer les infos de cache aux fichiers "minimisés" (actuellement, ça semble pas marcher)
- concaténer tous les CSS et tous les JS en un seul fichier, si possible à la volée
- compresser les CSS et JS
- ajouter un peu de gestion de cache, par exemple lors que j'édite un post depuis l'admin, il devrait s'assurer qu'il est invalidé dans le cache. Actuellement, seul l'ajout de commentaires fait cela...
- voir si je peux aussi optimiser la taille d'une page, avec des filtres pour supprimer les espaces inutiles etc (HTML::Minifier doit sans doute exister :D)

Voilà... J'espère arriver à faire les points ci-dessus, particulièrement la partie gzip des css/js... Si j'y arrive, je ferai évidemment un post avec les explications et le code.

++

T.
 

Syncroniser des images sur un iPad depuis Linux

ajouté le 01.06.2011 dans Geek World • par SwissTenguCommentaires (0)
Tags: ipad python linux

Ayant reçu il y a peu un iPad première génération de la part d'un généreux donateur, je me suis mis en tête de mettre des images dessus depuis mes linux.

Première constatation: y a rien (de stable/fonctionnel) qui supporte la synchronisation d'images vers des iTrux.

Deuxième constatation: nos amis Apple sont des enfoirés, et des grands adeptes du vendor-locking - mais c'est pas nouveau.

A force de fouiller le net, je suis tombé sur un thread d'un forum ubuntu. Il m'a apporté une solution provisoire:
ayant rooté mon iPad, je pouvais employer rsync pour poser les fichiers, et supprimer la bdd.
Seul problème: l'app devait refaire TOUTE la base de donnée lors de son premier relancement. Pas du tout gérable, si on a plus de 20 images (lent, mais LENT).

J'ai donc décider de comprendre comment la base sqlite est gérée, ce qui est nécessaire à l'application pour afficher les images, etc.

De là est né un petit script python d'environ 100 lignes, utilisable sous forme de librairie. Elle nécessite le support Fuse dans le kernel, ainsi que le package "ifuse", permettant de monter les iTrux sur Linux.

Voici le code:
Code: python
#!/usr/bin/env python
#-*- coding: utf-8 -*-

import sqlite3
import subprocess
import os
import shutil
import time
import sys
from PIL import Image

class synciPad():

  def __init__(self):
    self.db_index = 0
    self.PhotoAlbumToPhotoJoin = 0
    self.img_dir = 'DCIM/100APPLE'
    self.ipad_root = os.path.join(os.environ['HOME'], 'ipad')
    self.db_file = os.path.join(self.ipad_root, 'PhotoData', 'Photos.sqlite')
    self.db_aux = os.path.join(self.ipad_root, 'PhotoData', 'PhotosAux.sqlite')
    self.ipad_img = os.path.join(self.ipad_root, self.img_dir)
    self.todelete = ['Caches/StackedImages/CachedStackedImage-1000000101.BKS']
    self.db = ''

  def prepare(self):
    if not (os.path.exists(self.ipad_root)) or not os.path.isdir(self.ipad_root):
      print 'Creating ~/ipad directory'
      os.mkdir(self.ipad_root)
    if not os.path.ismount(self.ipad_root):
      print 'Mounting your iPad'
      subprocess.call(('ifuse %s' % os.path.basename(self.ipad_root) ).split())
    if not os.path.ismount(self.ipad_root):
      print
      print 'Unable to mount your device - aborting'
      print
      sys.exit(1)

    print 'Connecting to sqlite databases'
    self.db = sqlite3.connect(self.db_file)
    self.aux = sqlite3.connect(self.db_aux)
    print 'Getting latest state'
    try:
      self.db_index = self.db.execute('select primaryKey from Photo order by primaryKey desc limit 1').fetchone()[0]
    except TypeError:
      self.db_index = 1

  def finish(self):
    print '''Removing files to ensure iPad will recreate its thumbs and indexes'''
    for f in self.todelete:
      fd = os.path.join(self.ipad_root, f)
      if os.path.exists(fd):
        print '''  -> %s''' % fd
        os.unlink(fd)
    print 'Closing database connections'
    self.db.close()
    self.aux.close()
    print 'Unmounting %s using SUDO.' % self.ipad_root
    subprocess.call( ( 'sudo umount %s'% self.ipad_root ).split() )
    print
    print 'All images are now sync-ed. You have to restart your iPad right now to enjoy them :)'
    print
    sys.exit(0)

  def sync_image(self, img):
    img_name = os.path.basename(img)
    stripped = os.path.splitext(img_name)[0]
    dst = os.path.join(self.ipad_img, img_name)
    _img = Image.open(img)
    width, height = _img.size

    if _img.format != 'JPEG':
      print '   Unable to manage this type of image: %s. Please provide me JPEG only!' % _img.format
      return False

    if os.path.exists(dst):
      print '   Cannot copy file %s: Already exists!' % dst
      return False
    print '''   Copying %s to %s''' % (img_name, dst)
    shutil.copyfile(img, dst)
    query = '''insert into Photo 
    (type, title, captureTime, width, height, userRating, flagged, thumbnailIndex, orientation, directory, filename, duration, recordModDate, savedAssetType)
    values (%(type)d, "%(title)s", 328281811.0, "%(width)s", "%(height)s", 0,0,%(index)d, 1, "%(directory)s", "%(filename)s", "0.0", %(now)f, 0)'''
    dic = {'type': 0, 'title': stripped, 'now': time.time(), 'width': width, 'height': height, 'index': self.db_index, 'filename':img_name, 'directory': self.img_dir}

    print '   Inserting datas in databases...'

    self.db.execute( query % dic )
    self.db.commit()
    self.db_index = self.db.execute('select primaryKey from Photo order by primaryKey desc limit 1').fetchone()[0]

    try:
      self.PhotoAlbumToPhotoJoin = self.db.execute('select rightOrder from PhotoAlbumToPhotoJoin order by rightOrder desc limit 1').fetchone()[0]+64
    except TypeError:
      self.PhotoAlbumToPhotoJoin = 1
    query = '''insert into PhotoAlbumToPhotoJoin(left, right, rightOrder) values (1, %(img_index)d, %(right)d)'''
    dic = {'img_index': self.db_index, 'right': self.PhotoAlbumToPhotoJoin}
    self.db.execute( query % dic )
    self.db.commit()

    query = '''insert into AuxPhoto (latitude, longitude) values ('', '')'''
    self.aux.execute(query)
    self.aux.commit()

    query = (  u'''insert into PhotoExtras 
      (foreignKey, identifier, sequence, value) 
    values 
      (%(img_id)d, 1, -1, "%(dir)s")''',
    '''insert into PhotoExtras 
      (foreignKey, identifier, sequence, value) 
    values 
      (%(img_id)d, 2, -1, "%(img_name)s")''',
    '''insert into PhotoExtras 
      (foreignKey, identifier, sequence, value) 
    values 
      (%(img_id)d, 3, -1, %(img_size)d)''',
    '''insert into PhotoExtras 
      (foreignKey, identifier, sequence, value) 
    values 
      (%(img_id)d, 6, -1," 
          streamtypedè@NSMutableDictionary")''',
    '''insert into PhotoExtras 
      (foreignKey, identifier, sequence, value) 
    values 
      (%(img_id)d, 10, -1, 1)
    '''
    )
    dic = { 'img_id':self.db_index, 
            'img_name': img_name,
            'dir': self.img_dir,
            'img_size': os.path.getsize(img),
          }

    q = [x % dic for x in query ]
    for r in q:
      self.db.execute(r)
    self.db.commit()

    print '   Done'
    return True
    

if __name__ == '__main__':
  if len(sys.argv) == 2:
    sc = synciPad()
    sc.prepare()
    for img in os.listdir(sys.argv[1]):
      img = os.path.join(sys.argv[1], img)
      sc.sync_image(img)
    sc.finish()
  else:
    print 'Please provide me a directory containing your images'
    sys.exit(1)


Certes, il lui manque quelques fonctions, comme la conversion des images -> JPG (à voir, c'est le seul type supporté par l'iPad), le redimensionnement des images, la détection de rotation si besoin... Mais là ça marche !

Je ferai évoluer cette "lib" dans ce sens, et la poserai peut-être même sur github.

Le code n'est pas parfait, y a sans doute moyen de faire plus propre, m'enfin bon. J'suis pas non plus une bête en Python ;).

Si vous avez des idées d'amélioration (outre celles que j'ai mentionnées plus haut), n'hésitez pas à m'en faire part.

++

T.
 

vous prendrez bien un gâteau ?

ajouté le 04.03.2011 dans News • par SwissTenguCommentaires (0)
Tags: cookies nouveautés python

Petite nouveauté sans grande importance sur le blog:
la petite case "se souvenir de moi" pour les commentaires. Elle vous posera trois cookies distincts, contenant:
- votre pseudo
- votre site web
- votre adresse email

Pour cette dernière donnée, j'ai joué les vilains et l'ai cryptée en employant PyCrypto. A priori, cela devrait protéger votre email.

Pour cet encryptage, je me suis basé sur un bout de code trouvé sur le net qui me plaît bien:
http://code.activestate.com/re...
Comme vous le voyez, il utilise l'AES, et signe encore les données encryptées.

Il est évident que, si vous ne cochez pas la case, le cookie ne sera pas posé. Par contre, je n'ai pas fait qu'il se supprime si vous la décochez après-coup (genre sur un autre commentaire).
Il vous faudra, pour le moment, aller nettoyer les trois cookies à la main.
Leur durée de vie est de 3 mois.

Enjoy!

T.
 

urlshortener

ajouté le 29.10.2010 dans News • par SwissTenguCommentaires (0)
Tags: python pylons service

Je me suis amusé un tout petit peu avec python/pylons : http://s.it-nux.ch/ est né.

D'aucun me diront "erf, une Nième version de tinyurl". Certes. mais là, c'est fait par moi :D.

Le tout est basé sur une table unique dans du sqlite3 - à priori ça devrait tenir, surtout que je suis convaincu qu'il ne devrait pas y avoir plus de 10 url par année dans ce truc (quoi que, je vais passer par là pour mon lien twitter... à voir).

Concernant le code, y a pas grand chose à dire. Y aurait juste le système employé pour réduire l'URL qui peut être marrant à montrer:

Code: python
from shorturl.model import *
import random

def shorten(url, salt=False):
  a = url.split('e')
  short = ''
  for c in a:
    short += str(len(c))

  if len(short) > 4:
    short = short[0:3]

  if salt:
    short += ''.join(random.sample('a b c d e f g h i j k l m n o p q r s t u v w x y z'.split(), 3))

  try:
    meta.Session.query(Url).filter(Url.short == short).one()
    shorten(short)
  except:
    print short
    return short


Comme vous le voyez, je m'arrange pour que l'identifiant est bien unique. A priori, je ne devrais pas vider la table - sauf si vraiment y a beaucoup de monde et/ou de spam (ouais, c'est le problème avec ce genre de service... akismet risque de me voir).

Y a aussi le recyclage d'url, genre Toto met "google.com", et un moment après Titi aussi - même url, même identifiant au final.

Bref. Un truc simple au possible, qui va sans doute subir quelques évolutions par la suite.

++

T.
 

Indexer avec xapian

ajouté le 20.03.2010 dans Pylons • par SwissTenguCommentaires (0)
Tags: xapian pylons python

Depuis quelques temps, je joue pas mal avec xapian pour indexer pas mal de contenus, dont ce blog :).

Voici un petit peu de code, basé sur la librairie xappy, permettant d'indexer les nouveaux posts, et de mettre à jour au cas où vous modifiez un post.

Code: python
import xappy
import os
import hashlib
def create_db(db):
  if not os.path.exists(db):
    print 'creating database'
    conn = xappy.IndexerConnection(db)
    conn.add_field_action('url', xappy.FieldActions.STORE_CONTENT)
    conn.add_field_action('title', xappy.FieldActions.STORE_CONTENT)
    conn.add_field_action('date', xappy.FieldActions.SORTABLE, type="date")
    conn.add_field_action('date', xappy.FieldActions.STORE_CONTENT)
    conn.add_field_action('text', xappy.FieldActions.INDEX_FREETEXT, language='fr', spell=True)
    conn.add_field_action('text', xappy.FieldActions.STORE_CONTENT)
    conn.close()
  pass

def do_index(post, database,update=False):
  create_db(database)           
  conn = xappy.IndexerConnection(database)                    
  doc_id = hashlib.md5( '%s-%s'%(post.title, post.id) ).digest()
                                 
  doc = xappy.UnprocessedDocument(id=doc_id)
  doc.fields.append(xappy.Field("url", '/blog/post/%s' % post.id))
  doc.fields.append(xappy.Field("title", post.title))
  doc.fields.append(xappy.Field("date", post.time))
  doc.fields.append(xappy.Field("text", post.content))
  if update:
    conn.replace(doc)
  else:
    conn.add(doc)
  conn.close()
  pass


Il suffit ensuite d'appeler la fonction avec update=True ou False de manière à soit mettre le document à jour, ou le créer.

Et voilà :)

T.