Génération de miniatures dans Nuxeo Platform à l'aide d'AWS Lambda


Mon 16 January 2017 Par Mariana Cedica

En décembre dernier, j'ai eu la chance d'assister à nouveau à la fantastique conférence re:Invent : cinq jours de présentations passionnantes, des sessions pratiques, des jeux et bien sûr des soirées (après tout, l'événement se déroule à Las Vegas !). C'est presque impossible de n'aborder qu'un seul sujet quant on voit la multitude de nouvelles choses à découvrir et à apprendre : machine learning, IoT, big data, conteneurs, sécurité, bases de données et architectures sans serveur.

Depuis que je suis tombée amoureuse d'AWS Lambda l'année dernière, j'essaye de participer à un maximum d'ateliers sur le sujet. La quantité d'applications gérées par Lambda est infinie, mais dans l'une des sessions, le sujet était le classique suivant : Création automatique de miniatures lorsqu'une image est ajoutée à un bucket AWS S3.

Je dis « classique » car c'est l'un des exemples dans la documentation officielle et les sources sont sur GitHub, Amazon Web Services - Labs (si vous ne connaissez pas les labs d'Amazon, je vous recommande vivement d'y jeter un œil !)

Ce sujet m'a donné une excellente idée : puisque Lambda sait déjà exploiter ImageMagick pour réaliser des opérations simples de traitement d'image, je peux intégrer cet élément à Nuxeo Platform et générer toutes les miniatures avec Lambda.

Cas d'utilisation

Supposons que vous exécutez une application Nuxeo de Digital Asset Management avec tous les fichiers binaires stockés dans S3, vous aurez peut-être besoin d'un traitement des images à chaque fois que vous uploadez une nouvelle image. Comme les blobs sont déjà dans S3, vous pouvez déléguer ce traitement à une fonction Lambda et stocker les résultats dans le même bucket.

Conception

Mon exemple concerne la génération de miniatures, mais celui-ci peut facilement être modifié pour répondre à vos besoins. Nous avons besoin des éléments suivants : à chaque fois que Nuxeo Platform enregistre une image dans le bucket, une fonction Lambda est appelée pour générer la miniature, la remettre dans le même bucket puis mettre à jour le document existant dans Nuxeo Platform avec ces informations. Bien sûr, puisque la miniature est déjà dans le bucket, nous n'aurons pas besoin de l'uploader à nouveau, mais simplement d'indiquer son emplacement.

Mais il y a un problème évident avec ce processus. Si la fonction Lambda est appelée par un s3:ObjectCreated:Put et que les miniatures sont à nouveau ajoutées dans le même bucket, comment éviter la création d'une boucle générant les miniatures à l'infini ?

Voici la solution. Amazon S3 supporte la création de répertoires à l'intérieur d'un bucket afin de grouper des objets et les fonctions Lambda peuvent être configurées pour être déclenchées par des événements associés à des répertoires spécifiques. Afin d'éviter la création d'une boucle infinie, on peut créer deux répertoires dans le bucket : un pour stocker les images originales et un autre pour placer les miniatures générées. On doit bien sûr s'assurer que Nuxeo Platform accède aux miniatures générées.

Nuxeo Platform enregistre les fichiers binaires dans S3 à l'aide de leur résumé en tant que clé (nom) et peut être configurée pour utiliser un répertoire donné du bucket (à l'aide de la propriété nuxeo.s3storage.bucket_prefix). Sur cette base, on doit déplacer les miniatures générées dans le répertoire utilisé par Nuxeo Platform. Appelons-le /nuxeo

Nous devons donc créer :

  1. Une fonction Lambda qui génère les miniatures dans un nouveau répertoire (/thumbnails) et qui est appelée à chaque fois qu'un nouvel objet est ajouté dans le répertoire /nuxeo. Cette fonction sera appelée par l'événement s3:ObjectCreated:Put.

  2. Une fonction Lambda qui déplace les nouveaux fichiers depuis le répertoire /thumbnails vers /nuxeo et appelle une opération dans Nuxeo Platform afin de mettre à jour les infos avec la miniature qui vient d'être générée dans le document original une fois le déplacement réalisé. Puisque la copie de l'objet déclenche un événement s3:ObjectCreated:Copy, la première fonction ne reçoit pas l'instruction de générer à nouveau les miniatures.

The structure of the bucket
La structure du bucket

The sequence diagram
Schéma séquentiel

Implémentation

Vous pouvez trouver le code source de cet exemple (avec les fonctions Lambda et leur fonctionnement sous Nuxeo) sur GitHub. Je ne vais pas m'attarder sur la partie configuration, mais n'oubliez pas de donner les permissions en lecture et en écriture nécessaires à l'utilisateur qui exécute les fonctions Lambda. Pour ce faire, vous pouvez associer la politique de sécurité requise par Nuxeo Platform afin d'accéder au bucket (comme expliqué dans la doc de configuration d'AWS).

Passons aux différentes étapes de l'implémentation (en partant du principe que mon serveur Nuxeo est déjà configuré pour stocker les fichiers binaires dans le bucket test-picture-views-with-lambda, répertoire /nuxeo.) :

  1. Désactivation de la génération originale des miniatures dans Nuxeo Platform

    Une contribution simple permet de désactiver les listeners qui génèrent les miniatures :

    <listener name="updateThumbListener" enabled="false"/> 
    <listener name="checkBlobUpdate" enabled="false" />
    
  2. Création de la fonction Lambda generateThumbnails

    Vous pouvez partir de zéro et créer une fonction Lambda en sélectionnant le projet "image-processing-service" ou simplement récupérer le code ici (n'oubliez pas de zipper le répertoire et d'uploader tous les modules existants car ils utilisent les bibliothèques ImageMagick liées). Configurez-le pour qu'il soit appelé lorsqu'un nouvel objet est créé par Nuxeo Platform :

    generateThumbnails

    Voici la partie la plus intéressante de la fonction :

    var digest = crypto.createHash('md5').update(data).digest('hex');
    var dstKey = 'thumbnails/' + digest;
    s3.putObject({
       Bucket: dstBucket,
       Key: dstKey,
       Body: data,
       ContentType: contentType,
       Metadata: {
           originalFileDigest:originalFileDigest
       }
    },
    next);
    

Elle stocke la miniature qui vient d'être générée à l'aide de son résumé sous forme de clé et enregistre le résumé de l'image originale pour que nous puissions retrouver ultérieurement le document dans Nuxeo Platform et mettre à jour les informations relatives à sa miniature.

  1. Création de la fonctionnalité Lambda moveThumbnails

Mettez à jour le code du fichier moveThumbnails.js : cette fonction déplace la miniature en la copiant vers le répertoire /nuxeo puis appelle dans Nuxeo Platform une opération mettant à jour les informations du document.

moveThumbnails

   var thumbnailDigest = srcKey.substring('thumbnails'.length + 1, srcKey.length);
  var newKey = 'nuxeo/' + thumbnailDigest;

  //Copy - Pasting the object won't trigger a 'putObject' event
  s3.copyObject({
      Bucket: dstBucket,
      Key: newKey,
      CopySource: srcBucket + '/' + srcKey
  }, (err, data) => {
      if (err) {
          console.log('Error copying file:' + err);
      } else {
          //get the object to read the original file digest stored in metadata
          s3.getObject({
                  Bucket: dstBucket,
                  Key: newKey
          }, function(err, data) {
                  if (err) console.log(err, err.stack);
                    else
                  updateThumbnails(data.Metadata['originalfiledigest'], thumbnailDigest);
                   });
             }
  });
  1. Création de l'opération SetThumbnail.java dans Nuxeo Platform

Vous pouvez créer le jar nuxeo-thumbnails-with-lambda et le déployer sur le serveur. Il contient l'opération et la contribution permettant de désactiver la génération originale des miniatures Nuxeo Platform. Puisque le S3 Binary Manager attend le résumé du blob en tant que clé de l'objet dans le bucket, nous devons transférer le résumé de l'image originale (pour trouver le document dans Nuxeo Platform) et le résumé de la miniature. Nous n'allons bien sûr pas télécharger le blob à nouveau puisque le fichier est déjà dans le bucket. Il nous suffira d'envoyer à Nuxeo Platform la commande de création d'un nouveau blob à l'aide du résumé et de définir celui-ci en tant que miniature.

Comme vous pouvez le voir dans le code ci-dessus, nous avons déjà les deux puisque nous avons défini le résumé de l'image originale en tant que métadonnée personnalisée S3 sur la miniature générée.

Metadata

  DocumentModelList docs = session.query(String.format("Select * from Document where content/data = '%s' ",originalFileDigest))
      for (DocumentModel doc : docs) {
          doc.addFacet("Thumbnail");
          BinaryBlob sb = new BinaryBlob(new LazyBinary(thumbnailDigest, "default", getCachingBinaryManager()),
                  thumbnailDigest, (String) doc.getPropertyValue("file:content/name"), "image/png", null,
                  thumbnailDigest, -1);
          doc.setPropertyValue("thumb:thumbnail", sb);
          session.saveDocument(doc);
            }

Et c'est tout.

Voyons le résultat !

Ajoutez une image dans Nuxeo Platform :

Upload image in Nuxeo

Et vous obtenez automatiquement sa miniature !

Thumbnail

Et voici la preuve que les fonctions Lambda ont fait tout le travail (bien sûr, n'hésitez pas à les essayer vous-même !) :

Thumbnail generated by Lambda

Thumbnail moved by Lambda


Tagged: AWS, Nuxeo Platform, How to