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 😉

3 réflexions au sujet de « Ruby On Rails : gérer un zip »

    1. Essai peut-être de mettre la boucle dans l’écriture du zip, tu éviteras ainsi l’ouverture multiples du fichier zip à écrire.

      Astuce non testée.

      Julien

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.