Environnement d’exécution différents entre l’utilisateur et le cron de l’utilisateur

Une simple brève pour rappeler qu’un utilisateur que ce soit root ou un autre n’a pas le même environnement d’exécution que le cron de cet utilisateur. En d’autres termes ce n’est pas parce qu’une commande s’exécute correctement lorsque que l’on est connecté en tant que l’utilisateur X que cette même commande s’exécutera correctement dans un cron de l’utilisateur X (test fait sous debian).

L’environnement étant différent l’accès à certaines commandes ne se fait pas toujours de la même façon.
Ceux sont souvent des scripts peu classiques, par exemple un script de renouvellement automatique pour let’s encrypt comme proposé ici, s’exécutera très bien en root en ne tapant pas le chemin complet vers le script mais pas via le cron de root si vous n’indiquez pas le chemin complet vers le script. Il faut donc penser à tester ces crons, de manière quasi systématique pour être certains de la bonne exécution de ceux-ci.

Et voilà 😉

Récupérer les dates de début et de fin d’un certificat ssl en ligne de commande

Si vous avez besoin de récupérer rapidement et simplement les dates de début et de fin de certificat ssl pour par exemple vérifier le temps qu’il vous reste avant l’expiration il y a plusieurs solutions :

La solution simple via le navigateur : Vous vous connectez sur l’url à vérifier et vous inspectez votre certificat à travers le navigateur, action simple et visuelle.

Oui mais pas facile à automatiser cela demande une action humaine.

Si vous souhaitez monitorer les dates de manière automatique vous pouvez passer par une ligne de commande qui vous retourne les dates du certificat, la voici :
echo | openssl s_client -showcerts -servername monUrl.fr -connect monUrl.fr:443 2>/dev/null | openssl x509 -inform pem -noout -text | egrep "Not After :|Not Before:"

Cette ligne de commande vous sort ce résultat :

Not Before: Mar 24 07:28:00 2017 GMT
Not After : Jun 22 07:28:00 2017 GMT

Il est simple par la suite de récupérer les 2 dates (Not before : date d’émission du certificat, Not after : date d’expiration du certificat) et de les stocker comme bon vous semble et de faire les calculs qui vont bien pour vous prévenir quand l’échéance du renouvellement arrive.

Et voilà 😉

Source : http://serverfault.com/questions/661978/displaying-a-remote-ssl-certificate-details-using-cli-tools

Google maps Autocomplete déclencher la recherche au chargement de la page

Si vous avez mis en place un champ autocomplete de google maps et que ce champs et prérempli au chargement de la page et que vous souhaitez déclencher la recherche en conséquence, voici une petite astuce.

Cela consiste une fois tous les éléments chargés à simuler flèche du bas puis entrée sur la zone de recherche (souvent un input). Problème comment savoir que tous les éléments sont chargés car faire cette simulation trop rapidement ne fonctionne pas et aucun évènement n’est lancé d’après la doc google.

Du coup l’astuce consiste à attendre que la liste de propositions soit chargés et ensuite simuler la touche flèche bas puis entrée comme suit :

// On vérifie que la liste est prete
liste_prete = setInterval(function(){
  if ($(".pac-container").html() != undefined && $(".pac-container").html() != ""){
    google.maps.event.trigger(document.getElementById("VOTRE_INPUT"), 'keydown', { keyCode: 40 });
    google.maps.event.trigger(document.getElementById("VOTRE_INPUT"), 'keydown', { keyCode: 13, triggered: true });
    clearInterval(liste_prete)
  }
}, 200);

Voilà

Enjoy 😉

Petite astuce sur les encodages de string en Ruby

Si vous rencontrez cette erreur : Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ASCII-8BIT, sur la manipulation de string en Ruby, par exemple en lecture depuis un fichier vous pouvez prendre votre string est l’encodée comme suit :

MA_STRING.force_encoding("UTF-8")

La string est encodé en utf-8, vous pourrez la manipuler sans erreurs.

Enjoy 😉

Rest Client et téléchargement de fichier

Si vous utilisez la gem Rest Client vue dans plusieurs précédents articles ou même sans cette gem si vous faites des requêtes via Net:HTTP par exemple, sur un projet Ruby, que vous souhaitez télécharger un fichier en streaming depuis une url et que vous rencontrez cette erreur : « \x8E » from ASCII-8BIT to UTF-8, il y a une astuce assez simple.

Le fichier téléchargé est en binaire il faut donc indiquer à l’objet File que nous souhaitons écrire du binaire comment faire ? Comme vu dans l’article sur les zip :

     File.open(CHEMIN_DU_FICHIER_A_ECRIRE, 'w') { |file|
        response = RestClient.get URI.encode("URL_DU_FICHIER_A_TELECHARGER") do |str|
        # On passe en mode de fichier binaire
        file.binmode
        # On écrit le contenu téléchargé dans le fichier
        file.write(str)
      end
    }

C’est le file.binmode qui permet à l’objet File de comprendre que l’écriture se fera en binaire.

Et voilà

Enjoy 😉

Mysql restart ne fonctionne pas

Bon un petit article sur un cas bien particulier et pas simple à résoudre.

Le contexte vous avez un serveur avec des crons, on va dire beaucoup de crons ou des crons exécutés souvent ainsi qu’une partition racine / assez petite par exemple 10Go, et bien au bout d’un moment cette partition se remplie de manière importante soit dû à un remplissage de votre dossier /tmp comme vu dans un article précédent où nous expliquons comment déplacer le /tmp, soit dû au fonctionnement du service cron comme vu dans un autre article sur le remplissage de /var/mail/utilisateur.

La rencontre du problème :

Et un beau matin, vous n’arrivez plus à vous connecter sur votre PhpMyAdmin ou encore vos applications web sont toutes en rades avec des erreurs comme « Can’t connect to local MySQL server through socket ‘/var/run/mysqld/mysqld.sock' ».

La solution en mode rapide :

Du coup on monte sur le serveur en ssh en root et un df -lh plus tard on s’aperçoit que la partition est pleine, ce qui explique le souci, il ne reste donc plus qu’à utiliser l’un des 2 articles cités ci-dessus pour répondre au problème (en priorité celui sur les mails moins compliqués et plus rapide pour récupérer de la place). Fin… ou pas.

Comment ça s’est passé pour nous ? :

Oui mais forcément si j’écris cet article c’est bien que ce n’est pas ce que nous avons fait. Non en tant que développeur nous on aime bien comprendre les choses et lire des logs avec des messages d’erreurs ça aide souvent à comprendre les choses. Donc on monte sur le serveur en ssh en root et on va faire un tour dans /var/log, on regarde un peu ce qui concerne mysql et on voit des choses comme des .gz au détour d’une extraction on se dit que c’est l’extraction qui a généré un dossier mysql ou pour une raison abstraite l’envie nous prend de supprimer le dossier mysql.
A ce moment là mysql est toujours en rade (car nous n’avons pas encore fait de df -lh et nous ne connaissons pas encore la raison du plantage de mysql) et nous avons fait un rm -rf de /var/log/mysql/ (Oui il y a des fois on prend des risques). Ensuite on fait notre df -lh et oui dans les logs on a vu quelque chose comme « no space left » et là on s’aperçoit que c’est l’espace disque qui manque, on vide les mails dans le fichier /var/mail/utilisateur et on se dit que dans une seconde ça sera ok donc on fait un /etc/init.d/mysql restart et là failed, on insiste et on redémarre la VM et là mysql failed encore.
Impossible on a fait de la place etc. On va voir les logs ? Bonne idée. C’est où déjà ? Dans le dossier /var/log/mysql/. Non tu te trompes ça n’existe pas ce dossier (et oui on a une mémoire de poisson rouge parfois). Et s’en suit une longue recherche sur notre ami Google.
On tombe sur un article de stack overflow. On tente le mysqld --basedir=/usr --datadir=/var/lib/mysql --user=mysql --socket=/var/run/mysqld/mysqld.sock et devinez quoi ça fonctionne mysql ce relance, on est tout content, on fait même un /etc/init.d/mysql restart et encore une fois c’est ok parfait.

Oui mais … :

Oui mais voilà on est au courant que nous n’avons pas relancer mysql dans les règles de l’art et pire que ça on sait qu’on prochain redémarrage de la VM, mysql n’arrivera pas à se lancer donc on veut comprendre. Et comme la loi de Murphy nous l’apprend et bien la vm a redémarré le lendemain pour une raison quelconque sur laquelle nous n’avions pas la main et forcément re-belotte mysql en rade. Il faut qu’on trouve pourquoi et après de longues recherches aussi stupide et intrépide que possible la réponse était simplement dans le fichier de conf qui précise le répertoire des logs d’erreurs mysql et oui dans le fichier /etc/mysql/my.cnf il y a une ligne qui loggue les erreurs : log_error = /var/log/mysql/error.log et ce fichier my.cnf n’est pas appelé lorsque l’on lance la commande mysqld --basedir=/usr --datadir=/var/lib/mysql --user=mysql --socket=/var/run/mysqld/mysqld.sock, c’est pourquoi elle fonctionne alors que la commande /etc/init.d/mysql restart utilise notre fameux fichier my.cnf c’est pourquoi elle ne fonctionnait pas à cause de la non existence du dossier /var/log/mysql/. Vous vous doutez bien que derrière on fait en root un mkdir /var/log/mysql puis on kill le process précédemment lancé en récupérant le process id via ps aux | grep mysql puis kill -9 ProcessID et enfin un /etc/init.d/mysql restart et là bonheur tout refonctionne comme au bon vieux temps.

Enjoy 😉

Source : http://serverfault.com/questions/782725/cant-start-mysql-service

Ruby On Rails : sauvegarde de données dans un fichier temporaire

En Ruby, Si vous souhaitez créer un fichier temporaire (c’est à dire qui sera détruit par le système à la fin de son utilisation), vous pouvez utilisez la classe Tempfile.

Par exemple, vous avez besoin de créer un fichier zip temporaire qui se détruira une fois téléchargé par l’internaute.

Regardons comment fonctionne la classe Tempfile, c’est très simple, nous utilisons Ruby On Rails 5.

Tout d’abord nous déclarons notre fichier temporaire avec un nom et oui cela lui est nécessaire pour générer un nom temporaire à partir de la string passer en paramètres et de plein d’autre facteurs comme surement le temps.

zip_tmp = Tempfile.new("fichier_zip")

Ensuite nous ouvrons le zip et injectons les fichiers souhaités dedans comme dans l’article sur la gestion des zip en Rails (pensez à installer la gem rubyzip) :

    require 'zip'
    zip_tmp = Tempfile.new("fichier_zip")
    # Ouverture du fichier zip en écriture en passant le chemin vers le fichier ici le chemin vers notre fichier temporaire
    Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
      |zipfile|
      # Insertion du contenu dans le fichier MonFichier lui même inséré dans le zip
      zipfile.get_output_stream("MonFichier") { |f| f.puts "ContenuDeMonFichier" }
    }

Vous aurez remarqué que le chemin du fichier temporaire zip_tmp se récupère naturellement par zip_tmp.path.

Après ce code nous avons un fichier temporaire qui contient notre fichier, ici pas besoin de « flusher » le fichier temporaire mais si nous avions un fichier temporaire csv écrit à partie d’une variable contenant le contenu il faut appliquer la méthode flush sur le fichier temporaire afin que l’écriture soit réellement réalisée sur le disque sinon impossible de faire appel à la méthode File.read sur le fichier temporaire comme par exemple :

    csv = "colonne1;colonne2;colonne3"
    csv_tmp = Tempfile.new("fichier_csv")
    csv_tmp << csv
    csv_tmp.flush
    p File.read(csv_tmp.path)

Si vous exécutez le code précédent sans l’appel à la méthode flush, la string retournée sera vide.

Envoyons maintenant le zip à notre internaute par la commande send_file :

    require 'zip'
    zip_tmp = Tempfile.new("fichier_zip")
    # Ouverture du fichier zip en écriture en passant le chemin vers le fichier ici le chemin vers notre fichier temporaire
    Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
      |zipfile|
      # Insertion du contenu dans le fichier MonFichier lui même inséré dans le zip
      zipfile.get_output_stream("MonFichier") { |f| f.puts "ContenuDeMonFichier" }
    }
    send_file zip_tmp.path, filename: "fichier_zip.zip", type: "application/zip", disposition: :inline

La commande send_file passe la main au serveur web pour envoyer le fichier créé, si vous utilisez apache2 le fichier temporaire sera détruit une fois que l’instance d’apache2 sera détruite, ce qui est plutôt bien suivant les cas.

On aurait pu utiliser la méthode send_data en lisant le contenu du fichier temporaire puis en supprimant le fichier temporaire à la main pour être sûr qu’il soit détruit comme suit :

    require 'zip'
    zip_tmp = Tempfile.new("fichier_zip")
    # Ouverture du fichier zip en écriture en passant le chemin vers le fichier ici le chemin vers notre fichier temporaire
    Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
      |zipfile|
      # Insertion du contenu dans le fichier MonFichier lui même inséré dans le zip
      zipfile.get_output_stream("MonFichier") { |f| f.puts "ContenuDeMonFichier" }
    }
    # Lecture du contenu du fichier temporaire zip
    contenu_zip_tmp = File.read(zip_tmp.path)
    # Destruction du fichier temporaire
    File.delete(zip_tmp.path) if File.exist?(zip_tmp.path)
    send_data contenu_zip_tmp, filename: "fichier_zip.zip", type: "application/zip", disposition: :inline

Le fichier temporaire sera supprimé avant l’envoi des données à l’internaute mais son contenu sera bien transmis mais personnellement dans ce cas je ne vois pas trop l’intérêt d’utiliser des fichiers temporaires s’il faut gérer la destruction soit même, l’utilisation d’une instance de la classe File ferait aussi bien.

Voilà pour l’utilisation de la classe Tempfile et des fichiers temporaires en Ruby.

Enjoy 😉

Aïe ma partition /tmp est trop petite, comment déplacer le répertoire /tmp sous debian sur une autre partition ?

Si vous vous retrouvez dans le cas où votre partition /tmp est trop petite, par exemple avec un cron qui demande beaucoup de place dans /tmp et oui cela peut arriver, il existe une astuce pour déplacer votre répertoire /tmp sur une autre partition où vous aurez plus d’espace disque. Je vais faire ici un résumé du forum suivant : https://debian-facile.org/viewtopic.php?pid=43446#p43446, si vous souhaitez plus de détails sur le sujet de nombreuses notions sont abordées sur ce forum.

Dans notre cas d’exemple, nous avons un disque séparé en plusieurs partitions : la partition racine / de 10Go contenant notre fameux dossier /tmp et une partition pour /home de 145Go.

Le problème est survenue de manière silencieuse au départ, un cron qui grossit en terme d’actions à réaliser comme des rsync et donc qui grossit en terme d’espace temporaire nécessaire tant que la partition / avait assez de place pas de soucis le disque se remplissait (pas entièrement) de fichiers temporaires et une fois le cron fini les fichiers était « réellement » supprimé.

Ce point est important, il existe plusieurs commandes pour évaluer la place prises sur une partition « df » qui vous retourne l’espace occupé ainsi que l’espace total et par une soustraction magnifique cette commande arrive à vous retournez l’espace libre (c’est incroyable ;)) et une seconde commande « du » (et son dérivé « ncdu » basé sur le même comportement que « du ») qui elle vous retourne l’espace occupé par le dossier passer en paramètre. Et bien ces 2 commandes n’ont pas le même comportement, la commande « df » tient compte des fichiers supprimés mais encore présent sur le disque car ouvert par un processus, ce n’est pas le cas pour « du » (et son dérivé « ncdu ») qui lui ne considère plus les fichiers supprimés même s’ils sont encore ouvert par un processus. Et là nous avons un souci car on peut passer beaucoup de temps à regarder quel dossier est en surpoids avec « du » sans jamais trouver la réponse.

Ce point expliqué passons au vif du sujet, comme vous l’imaginez le problème n’est réellement apparu que lorsque le cron a saturé la partition racine est a donc fait « planté » le serveur.

A partir de là et après de multiples enquêtes à base de lsof | grep deleted pour récupérer la liste des fichiers effacés encore ouvert par un processus puis un ls -alh /proc/Processus_ID/fd/ pour voir où le ou les fichiers en question étaient stockés, je me suis aperçu que le dossier fautif était toujours /tmp.

Ce faisant j’ai fait des recherches et suis tombé sur ce forum traitant de debian, et voici comment j’ai déplacé le dossier temporaire du serveur debian.

J’ai utilisé la méthode dite « commune » dans le post ci-dessus (mais pas toutes les étapes, par exemple je ne suis pas passé en mode failsafe et oui serveur de prod oblige on évite les redémarrages). Je me suis donc mis en root puis assuré qu’aucun processus n’avait besoin de /tmp avec ps aux | grep tmp, s’il y en a il faut « killé » les processus avec kill -9 ProcessusId. Ensuite j’ai créer mon nouveau répertoire qui allait accueillir le contenu de /tmp (pas sur la partition racine on est d’accord sinon ça ne sert à rien) mkdir /home/MonCheminVersLeNouveauRepertoireTmp (ce dossier peut être préfixé par un point pour le caché ex : /home/MonCheminVersLeNouveauRepertoireTmpCaché/.tmp). On lui met les bons droits pour en faire un répertoire temporaire en bonne et dû forme chmod -R 1777 /home/MonCheminVersLeNouveauRepertoireTmp (Pour plus de détails sur le 1 de 1777 voire le forum). Jusque là tout va bien nous n’avons pas modifié le comportement de notre debian mais là nous allons voir le changement dans pas longtemps.
Avec toutes les précautions et le stress qu’il faut j’ai lancé un rm -rf /tmp, à cet instant il n’y a plus grand chose qui fonctionne (par exemple passenger pour les rubyistes) même la simple complétion des chemins lorsque l’on fait « tab » affiche une erreur très inquiétante, donc très rapidement je lance ln -s /home/MonCheminVersLeNouveauRepertoireTmp /tmp. A ce moment là la complétion avec « tab » refonctionne ouf c’est réparé et bien pas tout a fait encore une fois passenger lui ne se relance pas automatiquement notament son module pour apache2 donc faire un petit /etc/init.d/apache2 restart n’est pas de trop et je pense que d’autres programmes doivent être dans le même cas à vous de voir.

Et voilà, essayer de mettre des gros fichiers dans /tmp et un df -lh, plus tard vous verrez que ce n’est plus la partition / qui se remplie mais bien la partition /home.

Enjoy 😉

Ruby On Rails : gérer un zip

Il arrive parfois d’avoir besoin de retourner plusieurs fichiers lors de l’appel d’une action par exemple un csv avec des infos de base de données et des fichiers image associés à ces entrées en base de données.

Pour cela nous pouvons utiliser un zip dans lequel on mettra tous les fichiers nécessaires. Mais comment générer un zip depuis rails ? Ça tombe bien il existe une gem pour cela : rubyzip.

Regardons comment elle fonctionne sur un projet en rails 5 pour générer un zip puis le transmettre et lire son contenu dans une seconde action.

Pour l’exemple nous aurons un model article avec comme attribut un titre, un contenu, une date de publication et un champ nom_image. À ces articles sont associées des images qui sont stockées dans le dossier db/images/NOM_IMAGE

Première étape : Générer le zip

Pour cela nous allons installer la gem rubyzip avec un gem 'rubyzip' à mettre dans le gemfile puis un bundle install.

Maintenant nous allons déclarer une nouvelle action dans le contrôleur articles qui va utiliser cette gem pour créer un fichier zip avec à l’intérieur le csv des infos des articles et les fichiers associés à ces articles. Commençons par générer le CSV (de nombreuses façons sont possibles pour le faire) :

  def exporter_zip
    # Entête du CSV
    csv = "Titre;Contenu;Date de publication;Nom image;\n" 
    # On boucle sur nos articles en base de données
    Article.all.each do |article|
      # Création du CSV avec les données de l'article en cours
      csv += "#{article.titre};#{article.contenu};#{article.date_de_publication.strftime("%Y-%m-%d") if article.date_de_publication};#{article.nom_image}\n"
    end
 
    # Ici la variable csv contient le contenu du csv
    render plain: csv
  end

Pensez à mettre votre route comme suit :

get 'articles/exporter_zip'

La génération du csv est terminée.

Passons à l’ajout de ce csv à notre zip grâce à la gem rubyzip.

Pensez à inclure la librairie zip en mettant require 'zip' dans votre contrôleur.

Modifions maintenant notre méthode d’export comme suit :

  def exporter_zip
    # Entête du CSV
    csv = "Titre;Contenu;Date de publication;Nom image;\n" 
    # On boucle sur nos articles en base de données
    Article.all.each do |article|
      # Création du CSV avec les données
      csv += "#{article.titre};#{article.contenu};#{article.date_de_publication.strftime("%Y-%m-%d") if article.date_de_publication};#{article.nom_image}\n"
    end
 
    # Ici la variable csv contient le contenu du csv
 
    # Création du fichier de destination situé dans le dossier
    zip_tmp = File.new("#{Rails.root}/db/mon_fichier.zip",  "w+")
 
    # Ouverture du fichier zip en écriture
    Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
      |zipfile|
      # Insertion du contenu de la variable csv dans le fichier articles.csv lui même inséré dans le zip
      zipfile.get_output_stream("articles.csv") { |f| f.puts csv }
    }
 
    # Envoi du fichier zip précédemment créé
    send_file "#{Rails.root}/db/mon_fichier.zip"
  end

Jusqu’ici pas de souci cela fonctionne bien.

Passons à l’ajout des fichiers images liés à nos articles.

Regardons comment modifier notre action :

  def exporter_zip
    # Création du fichier de destination situé dans le dossier
    zip_tmp = File.new("#{Rails.root}/db/mon_fichier.zip",  "w+")
 
    # Entête du CSV
    csv = "Titre;Contenu;Date de publication;Nom image;\n"
    # On boucle sur nos articles en base de données
    Article.all.each do |article|
      # Création du CSV avec les données
      csv += "#{article.titre};#{article.contenu};#{article.date_de_publication.strftime("%Y-%m-%d") if article.date_de_publication};#{article.nom_image}\n"
 
      # Ouverture du fichier zip en écriture
      Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
        |zipfile|
        # Ajout du fichier image lié à l'article dans le fichier zip en cours de création
        zipfile.get_output_stream(article.nom_image) { |f| f.puts File.read("#{Rails.root}/db/images/#{article.nom_image}") }
      }
    end
 
    # Ici la variable csv contient le contenu du csv
 
    # Ouverture du fichier zip en écriture
    Zip::File.open(zip_tmp.path, Zip::File::CREATE) {
      |zipfile|
      # Insertion du contenu de la variable csv dans le fichier articles.csv lui même inséré dans le zip
      zipfile.get_output_stream("articles.csv") { |f| f.puts csv }
    }
 
    # Envoi du fichier zip précédemment créé
    send_file "#{Rails.root}/db/mon_fichier.zip"
  end

Et voilà nous venons de créer un fichier zip que l’on transmet par la suite.

Si vous souhaitez que ce fichier soit temporaire nous verrons cela dans un prochain article sur les TempFile.

Seconde étape télécharger et extraire ce fichier

Pour cela nous allons créer pour l’exemple une seconde application de test qui ne fera que télécharger le fichier depuis la première application et extraire le zip pour pouvoir manipuler les fichiers qui sont à l’intérieur.

Dans cette application numéro 2 que nous appellerons APPLI2 nous n’aurons qu’une action dans l’application controller qui téléchargera le zip de puis la première application que nous appellerons APPLI1 et en extraira les fichiers.

Dans APPLI2 nous allons utiliser la gem rest-client.

On place donc gem 'rest-client' dans le gemfile puis un bundle install.

Définissons notre route get 'application/essai' puis notre action comme suit :

  def essai
    # Endroit où sera téléchargé le fichier zip
    fichier_zip = "#{Rails.root.to_s}/db/articles.zip"
    # On télécharge le zip depuis l'APPLI1 qui tourne sur (localhost:3000) et on le sauvegarde dans db/articles.zip
    File.open(fichier_zip, 'w') { |file|
      response = RestClient.get URI.encode("http://localhost:3000/articles/exporter_zip") do |str|
        # On passe en mode de fichier binaire
        file.binmode
        # On écrit le contenu téléchargé dans le fichier
        file.write(str)
      end
    }
    render plain: "OK"
  end

A ce moment là nous avons bien téléchargé le fichier zip et stocké ce dernier dans db/articles.zip

Maintenant lisons le fichier afin d’extraire les fichiers inclus dans le zip. Pour cela, ajoutez dans le gemfile gem 'rubyzip', faites un bundle install et ajoutez require 'zip' en haut du contrôleur application.

Modifions notre action :

  def essai
    # Endroit où sera téléchargé le fichier zip
    fichier_zip = "#{Rails.root.to_s}/db/articles.zip"
    # On télécharge le zip depuis l'APPLI1 qui tourne sur (localhost:3000) et on le sauvegarde dans db/articles.zip
    File.open(fichier_zip, 'w') { |file|
      response = RestClient.get URI.encode("http://localhost:3000/articles/exporter_zip") do |str|
        # On passe en mode de fichier binaire
        file.binmode
        # On écrit le contenu téléchargé dans le fichier
        file.write(str)
      end
    }
 
    # Ouverture du fichier zip
    Zip::File.open(fichier_zip) do |fichiers|
      # On récupère le csv des articles
      fichier = fichiers.glob('articles.csv').first
      # Lecture du contenu du fichier CSV
      contenu_csv = fichier.get_input_stream.read
 
      # Action sur le contenu du csv
      cpt = 0
      contenu_csv.split("\n").each do |ligne_csv|
        if cpt != 0
          champs = ligne_csv.split(";")
          # ICI champs[0] contient le titre, champs[1] contient le contenu, ...
          image = fichiers.glob("#{champs[3]}").first
          if image
            # Action sur le fichier image par exemple le sauvegarder dans un dossier
          end
        end
        cpt += 1
      end
    end
    render plain: "OK"
  end

Et voilà nous venons de voir comment manipuler des zip avec Ruby On Rails pour les émettre puis les traiter.

Enjoy 😉

Ruby on Rails helper_method et view_context, quels usages ? Quelles différences ?

Sur une application Ruby on Rails, il arrive parfois que nous ayons besoin d’utiliser une méthode dans un contrôleur et dans une vue.

Plusieurs solutions existent d’un côté les helper_method, de l’autre les méthodes écrit dans des helpers et utilisées dans la vue par un simple appel qui se complexifie un peu lorsque l’on veut l’utiliser dans le contrôleur où il faut faire précéder l’appel de la méthode view_context.

Essayons de prendre un cas de figure assez simple, nous souhaitons retourner l’objet entier de la personne actuellement connecté sur notre application. Pour cela nous avons une session avec une variable de session utilisateur_id qui contient l’identifiant de notre personne dans la table utilisateurs.

Le code pour retourner l’objet est assez simple :

def utilisateur_courant
  Utilisateur.where(id: session[:utilisateur_id]).take
end

Là où cela se complique c’est où placer ce code car j’en ai besoin à la fois dans mon contrôleur mais aussi dans ma vue. Dans le contrôleur pour faire du contrôle d’accès aux autres objets par exemple, et dans ma vue pour afficher « Bonjour » suivi du prénom de la personne.

La première information est la sémantique du code écrit ici, nous faisons une requête pour récupérer un objet c’est typiquement ce que doit faire un contrôleur donc pas de doute ce code doit être de base définit dans le contrôleur puis rendu accessible à la vue. Nous allons donc définir ce code dans un contrôleur par exemple l’application contrôleur (pour que la méthode soit accessible dans tous les autres contrôleurs) puis toujours dans l’application contrôleur nous allons indiquer que c’est un helper_method, ce qui nous donnerai en Rails 5 :

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
 
  helper_method :utilisateur_courant
 
  def utilisateur_courant
    Utilisateur.where(id: session[:utilisateur_id]).take
  end
end

A partir de là vous pouvez utiliser la méthode soit dans la vue en faisant par exemple :

<%= "Bonjour #{utilisateur_courant.prenom}" %>

Soit utiliser la méthode dans le contrôleur pour faire :

  def redirige_si_prenom_julien
    if utilisateur_courant && utilisateur_courant.prenom == "Julien"
      redirect_to "/"
    else
      render plain: "OK"
    end
  end

Amélioration possible, si la méthode utilisateur_courant est appelée plusieurs fois par action, cela génère plusieurs requête sql alors que dans une même action la session ne change pas trop en général, pour éviter cela nous pouvons faire :

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
 
  helper_method :utilisateur_courant
 
  def utilisateur_courant
    @utilisateur ||= Utilisateur.where(id: session[:utilisateur_id]).take
  end
end

Avec cela, la requête sql s’exécutera au premier appel de notre méthode utilisateur_courant et stockera le temps de l’action le résultat de la requête dans la variable @utilisateur, si sur le temps de l’action nous faisons un second appel à utilisateur_courant, la méthode retournera directement le contenu de la variable @utilisateur et donc ne fera pas une seconde requête sql inutile dans bien des cas.

Ok très bien donc la sémantique nous permet de voir où placer le code et quand faire des helper_method dans le contrôleur, dans ce cas quelles sont les types de méthodes à mettre dans les helpers de vue ?

Déjà quel est l’objectif des helpers ?

Les helpers permettent de :

  • partager du code entre les vues
  • d’alléger les vues
  • de placer la complexité ailleurs que dans la vue (par exemple un case when)

Prenons un exemple :

Dans mon application rails j’ai un objet article avec comme attribut titre qui est une string, contenu qui est un texte et date_de_publication qui est une date. Il fréquent de vouloir afficher le statut de l’article en fonction de la date de publication, si la date n’est pas affectée le statut est brouillon, si la date est dans le futur le statut est programmé, si la date est dans le passé le statut est publié. Nous avons donc à partir d’un champ 3 affichages possibles différents dans notre vue. On peut donc vite se retrouver avec quelque chose comme :

<h1>Articles#index</h1>
<ul>
<% @articles.each do |article| %>
  <%
  if article.date_de_publication.nil?
    statut = "brouillon"
  elsif article.date_de_publication > Date.today
    statut = "programmé"
  elsif article.date_de_publication <= Date.today
    statut = "publié"
  end
  %>
  <li><%= "#{article.titre} : #{statut}" %></li>
<% end %>
</ul>

Bon là ce n’est pas le top car la complexité, ici les 3 tests de la valeur de date_de_publication, est directement dans la vue si nous souhaitons « laissé de côté » cette complexité en faisant confiance à une méthode, cela permettrait lors d’une lecture ultérieure du code de ne pas avoir en tête cette complexité si nous n’en avons pas besoin.

Nous allons donc déplacer cette complexité dans un helpers et oui c’est notamment à cela que ça sert, de plus ces statuts ne sont valables pas seulement pour les articles mais aussi pour d’autres modèles comme les catégories ou autre, nous obtenons donc dans notre helper :

module ArticlesHelper
  def statut(date)
    if date.nil?
      statut = "brouillon"
    elsif date > Date.today
      statut = "programmé"
    elsif date <= Date.today
      statut = "publié"
    end
  end
end

et dans notre vue, un code bien plus « léger » :

<h1>Articles#index</h1>
<ul>
<% @articles.each do |article| %>
  <li><%= "#{article.titre} : #{statut(article.date_de_publication)}" %></li>
<% end %>
</ul>

Voilà notre helper est créé, maintenant imaginons une mise à jour de la date de publication en ajax qui en retour nous retournerai le nouveau statut de l’article ça serait pas mal pour mettre à jour dans notre vue. Un peu de code sur la vue liste des articles :

<h1>Articles#index</h1>
<ul>
<% @articles.each do |article| %>
  <li>
    <%= "#{article.titre} :" %>
    <span>
      <%= "#{statut(article.date_de_publication)}" %>
      <%= link_to publie_article_url(article), method: :post, confirm: "Publié l'artcile ?", class: "publie_article", :remote => true do %>
        Publié maintenant
      <% end %>
    </span>
  </li>
<% end %>
</ul>
<script>
  $(function(){
    $('.publie_article').bind('ajax:success', function(evt, data, status, xhr) {
      $(this).parent().html(data.statut);
    });
  });
</script>

Ici nous avons utiliser le remote: true qui permet de faire de l’ajax de manière rapide avec rails plutôt pas mal au passage ;). Après il nous faut « binder » le moment où la requête se termine en état succès dans la partie javascript (nous ne traitons pas le cas ajax:error car ce n’est pas le but de cet article). L’action publie_article du controller articles permet de mettre la date du jour en date de publication et de retourner le statut de l’article après mise à jour de celui-ci via notre méthode de helper pour mise à jour de la donnée dans la vue. Regardons du côté controller :

  def publie_article
    article = Article.find(params[:id])
    article.update(date_de_publication: Date.today)
    render json: {"statut": view_context.statut(article.date_de_publication)} # Ici nous faisons appel à notre helper précédemment défini via la méthode view_context nous aurions aussi pu faire helpers.statut(article.date_de_publication) depuis rails 5 qui est peut-être plus parlant 
  end

Dans le fichier de routes nous avons ajouté post 'articles/publie_article/:id', to: 'articles#publie_article', as: 'publie_article' pour accéder à notre action.

Voilà nous avons vu les 2 cas d’utilisation d’une méthode à la fois dans la vue et le contrôleur, petite info lors de l’appel à view_context.statut cela instancie une nouvelle vue à chaque appel donc ne pas en abuser est une bonne chose pour les performances.

En conclusion les 2 solutions sont bonnes ce qui est primordial est de placer le code au bon endroit en fonction de ce qu’il fait.
Ici l’exemple aurait pu être réalisé autrement avec un concern de modèle inclut dans les X modèles nécessitant la méthode d’instance statut qui retournerait le statut en fonction de la date_de_publication mais avec notre helper l’avantage est que si les modèles ont l’attribut date_de_publication nommée différemment les uns des autres cela fonctionne toujours, à voir donc en fonction des cas et du contexte.

Source : http://apidock.com/rails/v4.2.7/AbstractController/Helpers/ClassMethods/helper_method, http://stackoverflow.com/questions/5130150/rails-how-to-use-a-helper-inside-a-controller