jbbarth's corner

Le webscraping est défini sur la page française de Wikipedia comme suit :

Le Web scraping (parfois appelé Harvesting) décrit généralement en informatique un moyen d’extraire du contenu d’un site internet, via un script ou un programme, dans le but de le transformer ou de changer son format pour permettre son utilisation dans un autre contexte.

J’en faisais régulièrement avant en mIRC scripting, rencontrant souvent des problèmes plutôt http que parsing : suivi des sessions, chunks, etc… Et puis j’étais à un séminaire ces derniers jours, et des collègues m’ont remotivé pour en faire, alors je commence quelques essais en Ruby.

Let’s go !

Installons scRUBYt!, un package qui vient par dessus Hpricot notamment (parser HTML). Les instructions officielles n’étant pas très concluantes, je me suis rabattu sur ce post qui a résolu tous mes soucis :-) Il faudra d’ailleurs que j’examine un de ces jours les avantages de Scrubyt par rapport à Hpricot seul, vues les difficultés d’installation, raisonnables mais un peu aggaçantes (et qui ne feront que s’ajouter aux futures difficultés lors d’installations sur d’autres distribs ou au boulot).

# gem install —version 1.7.1 ParseTree
# gem install —version 3.6.3 RubyInline
# gem install —version 1.1.6 ruby2ruby
# gem install ParseTreeReloaded
# gem install RubyInlineAccelleration
# gem install scrubyt

Parfois il faut relancer le gem install une ou deux fois, mais ça finit par passer normalement.

Ensuite, premier essai (tiré d’ici) :

require 'rubygems'
require 'scrubyt'

google_data = Scrubyt::Extractor.define do
   fetch 'http://www.google.com/ncr'
   fill_textfield 'q', 'ruby'
   submit

   link "Ruby Programming Language/@href" 
   next_page "Next", :limit => 2
end

puts google_data.to_xml
# ruby test_scrubyt.rb 
/usr/local/lib/site_ruby/1.8/rubygems.rb:139:in `activate': can't activate RubyInline (= 3.6.3), already activated RubyInline-3.7.0] (Gem::Exception)
    from /usr/local/lib/site_ruby/1.8/rubygems.rb:155:in `activate'
    from /usr/local/lib/site_ruby/1.8/rubygems.rb:154:in `each'
    from /usr/local/lib/site_ruby/1.8/rubygems.rb:154:in `activate'
    from /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
    from test_scrubyt.rb:2

Oopsie !
J’ai donc désinstallé la version 3.7.0 via le choix 2 dans la commande :

# gem uninstall RubyInline

Et là hop !

# ruby test_scrubyt.rb 
/usr/lib/ruby/gems/1.8/gems/scrubyt-0.3.4/lib/scrubyt/core/scraping/filters/text_filter.rb:25: warning: don't put space before argument parentheses
http://www.google.com/search?hl=en&ie=ISO-8859-1&q=ruby
http://www.google.com/search?hl=en&ie=UTF-8&q=ruby&start=10&sa=N
<root>
  <link>http://www.ruby-lang.org/</link>
  <link>http://www.ruby-lang.org/en/20020101.html</link>
  <link>http://en.wikipedia.org/wiki/Ruby_programming_language</link>
  <link>http://en.wikipedia.org/wiki/Ruby</link>
  <link>http://www.rubyonrails.org/</link>
  <link>http://www.rubys.com/</link>
  <link>http://www.rubycentral.org/</link>
  <link>http://www.w3.org/TR/ruby/</link>
  <link>http://www.zenspider.com/Languages/Ruby/QuickRef.html</link>
</root>

Excellent début !

Après un petit patch de /usr/lib/ruby/gems/1.8/gems/scrubyt-0.3.4/lib/scrubyt/core/scraping/filters/text_filter.rb:25 (tu parles d’un patch, j’ai juste supprimé un espace… :D), je compte bien mener d’autres essais, notamment ce dont j’ai parlé dans le résumé :-) Je mettrai les résultats ici !

Comments: 0 (view/add your own) Tags: ruby, tech

Imaginons qu’on veuille ajouter 10000 jours au 26/06/2008. Date.new(2008,6,26) permet de créer un premier objet date. La méthode “+” permet ensuite d’ajouter des jours, et pour info “>>” d’ajouter des mois. Modulo un require “Date” au début, on a tout ce qu’il faut ! Il faut juste penser à faire un “to_s” à la fin pour sortir une chaine de caractères, sauf si on utilise “puts” qui l’appelle tout seul…

Au final, même pas besoin de faire un script, une oneliner suffit :

root@vds# ruby -e ‘require “Date”; puts Date.new(2008,6,26) + 10000’
2035-11-12

Même genre de solution pour trouver un écart de dates, ou bien sûr de nombres :

root@vds# ruby -e ‘require “Date”; puts Date.new(2008,9,19) – Date.new(2008,6,26)’
85
root@vds# ruby -e ‘puts 10000 – 85’
9915

Dans 10000 jours depuis le 26/06, c’est à dire 9915 jours à compter d’aujourd’hui, nous serons le 12 novembre 2035 ! Merci Ruby :-)

Comments: 0 (view/add your own) Tags: ruby, tech

Heureusement que j’ai des heures de boulot pour m’occuper de mon blog et de ma culture personnelle.

Je commence à avoir de plus en plus de spams, j’ai donc naturellement cherché un antispam pour Mephisto. J’ai trouvé une sorte d’inverse captcha ici, l’installation est détaillée ici.

Le principe est vraiment excellent et certainement infaillible pour des sites confidentiels (au sens “trop peu fréquentés pour que les spammers s’y intéressent de près”) : la plupart des sites vous obligent à entrer une suite de caractères tirés d’une image qu’un robot ne saurait pas lire. Pour un “captcha inversé” le principe est diamétralement opposé : il y a un champ (mail par exemple) qui sert de piège aux robots, et que les humains ne doivent pas remplir : au pire un message les informe, au mieux c’est caché via du Javascript ou du CSS. Si ce champ est rempli le commentaire n’est pas validé.

J’avais oublié de publier cet article : ce système tourne sur ce blog depuis mi-septembre, et plus un seul spam depuis. Yeah ;-)

Là où je bosse, on a la chance (?) de disposer d’un SAN, c’est-à-dire d’un réseau dédié au stockage. En l’occurence c’est un réseau fibre, et tous les flux passent par deux switches fibre McData Sphereon 4500… qu’il faut surveiller d’une manière ou d’une autre.

En l’occurrence on surveille tout ça par Nagios. L’ancien script (qui doit faire partie des nagios plugins du site officiel ?) vérifiait les interfaces une par une, ce qui est gênant dans un environnement évolutif (oubli d’interfaces non surveillées!). Il faisait d’autres choses (check trafic rx/tx, errors) que mon script ne fait pas, mais il ne demande qu’à être amélioré ;-)

Voici donc un script de remplacement qui check toutes les interfaces d’un coup. Gros avantage : on passe les ports non branchés en exception, mais si un port est vu “up” et qu’il est en exception le script sort en Critical. Cela force à maintenir la liste des exceptions à jour ;-)

PS: Il faut bien sûr que ruby soit installé sur votre serveur de supervision.

#!/usr/bin/ruby

# Dernière modif: 19/11/2008
# Script de test des ports pour les switches fibre
# Jean-Baptiste BARTH <jeanbaptiste.barth@gmail.com>

# Remonter les infos à la main (sous l'user "nagios")
# $ snmpwalk -c public -v 2c switch_san_a .1.3.6.1.4.1.289.2.1.1.2.3.1.1.2
#       SNMPv2-SMI::enterprises.289.2.1.1.2.3.1.1.2.1 = INTEGER: 2
#       SNMPv2-SMI::enterprises.289.2.1.1.2.3.1.1.2.2 = INTEGER: 2
# ...
# => 2 = OK, 6 = down, 13 = info not available

unless ARGV.length >= 2 && ARGV.length <= 3
  puts "Mauvais format :"
  puts "\t./check_fc_setra.rb HOST COMMUNITY [EXCLUSIONS]"
  puts "Les ports exclus évitent des remontées d'alertes pour les ports non branchés."
  puts "Exemple: ./check_fc_setra.rb switch_san_a public 1,4,9"
  exit 2
end

# Valeurs prises dans utils.sh
STATE_OK=0
STATE_WARNING=1
STATE_CRITICAL=2
STATE_UNKNOWN=3
STATE_DEPENDENT=4

# Parsing arguments
host = ARGV.shift
community = ARGV.shift
exclusions = (ARGV.shift || "none").split(",")

# Passage de la commande
command = `snmpwalk -c #{community} -v 2c #{host} .1.3.6.1.4.1.289.2.1.1.2.3.1.1.2`
result = []
command.each do |line|
  #puts "DEBUG snmpwalk: "+line
  matches = line.match /\.(\d+) = INTEGER: (\d+)/
  result.push [matches[1], matches[2]]
end

# Traitement de la sortie
not_ignored = result.select{|x| !exclusions.include? x[0]}
interfaces_down = not_ignored.select{|v| v[1] == "6"}.map{|x| x[0]}
interfaces_ok = not_ignored.select{|v| v[1] == "2"}.map{|x| x[0]}
interfaces_unknown = not_ignored.map{|x| x[0]} - interfaces_ok - interfaces_down
interfaces_ok_not_normal = (result - not_ignored).select{|v| v[1] == "2"}.map{|x| x[0]}
# Output
puts "Link DOWN on interfaces : "+interfaces_down.join(",") unless interfaces_down.empty?
puts "Link UP on IGNORED interfaces : "+interfaces_ok_not_normal.join(",")+"\n!!!CHANGE THE SERVICE CONFIG!!!" unless interfaces_ok_not_normal.empty?
puts "State UNKNOWN : "+interfaces_unknown.join(",") unless interfaces_unknown.empty?
puts "Link UP on interfaces : "+interfaces_ok.join(",") unless interfaces_ok.empty?
puts "Ignored: "+exclusions.join(",")
if !interfaces_down.empty? || !interfaces_ok_not_normal.empty?
  exit STATE_CRITICAL
elsif !interfaces_unknown.empty?
  exit STATE_UNKNOWN
else
  exit STATE_OK
end

Ma version de Simplelog, disponible sur mon espace Github, supporte désormais l’upload d’images, utilisables ensuite dans les posts ou pages de son choix :

Pour que le tout fonctionne, il faut disposer sur son serveur d’une bibliothèque de traitement d’image pour Ruby. Personnellement j’ai choisi ImageScience pour sa simplicité, mais je viens de me rendre compte qu’il y avait des bugs sur les images PNG. Je vais voir si je peux passer à autre chose pour la suite.

To be continued ;-)

EDIT: je suis passé à RMagick, tout a l’air de marcher pas mal. D’ailleurs pour ma version de Simplelog, c’est mis comme processeur par défaut (pour le moment en dur dans la classe, qui sait peut-être bientôt en configurable) :

class Image < ActiveRecord::Base
 
  has_attachment :content_type => :image,
                  :storage => :file_system,
                  :max_size => 5.megabytes,
                  :resize_to => '740x400>',
                  :thumbnails => { :thumb => '100x100>' },
                  :path_prefix => 'public/assets',
                  :processor => 'Rmagick'
 
  validates_as_attachment
 
end

En ce moment j’ai trop d’articles techniques à publier, souvent trop en bazar, ou trop long pour être publiés dans une demi-colone de ce blog. De plus je risque de quitter mon boulot dans quelques mois, et tout ce que j’ai pû documenter là-bas sera perdu pour moi si je ne le récupère pas avant. Bref, j’ai besoin d’un wiki, simple d’utilisation, avec suivi des révisions, formatage en Textile, un peu comme celui de Remine.

Une contrainte supplémentaire, qu’il soit en Ruby on Rails, éventuellement Merb. Pas par idéologie, croyance en REST, ou quelconque connerie de ce genre (et certains en tiennent une couche à ce niveau quand on voit la liste rails-france), mais plutôt parce que j’en ai marre de maintenir 50 technos hétérogènes sur mon serveur. Mon blog, mon gestionnaire de projets tournent déjà en Rails. Ce sera bientôt le cas du site chanmasters.com que je réécris en ce moment. Bientôt le cas également de ma gallerie de photos, qui tourne déjà avec des scripts ruby mais pas administrable en mode web. Donc dommage pour Dokuwiki qui a priori me convenait parfaitement…

J’ai essayé Instiki sans être réellement convaincu, je ne saurais pas bien expliquer pourquoi. Et puis, j’aurais des modifs à faire dessus pour qu’il me convienne (templates trop épurées à mon goût, affichage “geeky” de certains champs, etc…). Ce qui implique un petit peu d’exploration du code, cf la suite. Je n’ai pas trouvé d’autres projets très actifs en Rails et qui me convienne (liste ici)…

En fait, le wiki de Redmine me convient parfaitement. Facile à “hacker” (je commence à connaître un peu le code même si je suis loin d’être un gourou comme les 3 ou 4 grands contributeurs réguliers), maintenu, communauté active, support de textile entre autres, jolis diff, etc. Nickel, à 4 petites choses près :

  1. le support Textile est un peu foireux parfois ; voir ici
  2. la coloration syntaxique est hideuse, j’aimerais bien remplacer CodeRay par autre chose ; voir ici
  3. j’aimerais bien que les hiérarchies soient faites automatiquement, quitte à rendre l’arborescence un peu profonde, pas grave ; voir ici
  4. j’aimerais bien un système de tags avec un nuage sur le côté ; voir ici, ici et ici

Bref, ça sent bon, et le choix est tout fait ; je documenterai dans le prochain article l’installation et la configuration de Redmine pour me servir de wiki :-)

Ce post fait suite à celui-ci.

Tout d’abord, on installe Redmine classiquement ; je passe volontairement les aspects DNS (création d’un sous-domaine, en l’occurence wiki.jbbarth.com), Apache (création du vhost), et “système” (script de démarrage de Mongrel, user et port adéquats, ce genre de choses) :

cd /home/app/jbbarth/
svn co http://redmine.rubyforge.org/svn/trunk _redmine-0.8-wiki
ln -s _redmine-0.8-wiki wiki
cd wiki/
rake db:migrate
rake redmine:load_default_data
rake config/initializers/session_store.rb

Après démarrage, on procède à une configuration basique de Redmine :

  • modification du user/pass admin
  • configuration générale dans Administration > Settings
  • création d’un projet public “wiki”, identifiant “wiki” ; tous les trackers décochés, et tous les modules sauf le module “wiki”
  • dans ce projet, on configurera la “Start page”, et surtout on la créera/remplira (sous peine d’avoir des erreurs 404 dans la suite)

Là commencent les choses “sérieuses”. Que voulons-nous ?

1) que l’accueil de Redmine se fasse sur la page de démarrage du wiki du projet “wiki”
Pour cela, nous allons éditer config/routes.rb, et remplacer l’accueil défini à la ligne “map.home” par :

  #map.home '', :controller => 'welcome'
  map.home '', :controller => 'wiki', :id => 'wiki'

Après redémarrage de l’instance, ça fonctionne !

2) suppression des liens inutiles pour un wiki ; en particulier la première tab “Overview/Aperçu”, et “Projects/Projets”, “My page/Ma page” et “Help/Aide” dans le menu en haut à gauche (nous n’aurons qu’un seul projet “wiki”)
Pour cela nous allons créer un thème à nous et cacher ces liens via du CSS, ce qui me parait bien suffisant : ils ne représentent aucun “danger”, c’est juste qu’ils perturbent la navigation dans le cadre d’une utilisation wiki-only. Voir donc ici pour la création d’un nouveau thème, et éventuellement ici pour des exemples de thèmes.
Nous créons donc un thème “wiki”, puis quelques lignes suffisent à la fin de public/themes/wiki/stylesheets/application.css :

/* Specific to wiki */
#top-menu .my-page, #top-menu .projects, #top-menu .help { display:none; }
#main-menu .overview { display:none; }

3) passage des patches cités dans l’article précédent

cd /home/app/jbbarth/wiki/
mkdir patches

a) passage de CodeRay à UltraViolet :

wget -P patches http://www.redmine.org/attachments/download/1698/syntax_highlighting.diff
patch -p0 < patches/syntax_highlighting.diff
wget -P patches http://www.redmine.org/attachments/download/1699/redcloth.diff
patch -p0 < patches/redcloth.diff
wget -P patches http://www.redmine.org/attachments/1700/ultraviolet_highlighter.zip
cd patches/
unzip ultraviolet_highlighter.zip
cat ultraviolet_highlighter/README.txt
apt-get install libonig-dev
gem install ultraviolet
cp -a ultraviolet_highlighter ../vendor/plugins/
cd ..

Les traductions ne sont pas bien passées, donc on édite à la main config/locales/en.yml et fr.yml, et on supprime les fichiers “.rej” correspondants.
b) pages parent automatiques :

wget -P patches http://www.redmine.org/attachments/download/2082/3108_automatic_parent_with_tests.diff
patch -p0 < patches/3108_automatic_parent_with_tests.diff

c) correction d’un petit bug de Redcloth :

wget -P patches http://www.redmine.org/attachments/download/1728/redcloth_arobas.diff
patch -p0 < patches/redcloth_arobas.diff

d) système de tagging :
Il y avait un patch proposé ici, mais il ne correspond pas vraiment à ce que je veux. Voici quand même une méthode pour l’appliquer sur une copie de travail SVN :

wget -P patches http://www.redmine.org/attachments/download/2060/wiki_page_categories_20090520.patch
sed -i -e 's#- redmine.orig/#- #g' -e 's#\+ redmine/#+ #g' -e 's#diff.*\.orig/\([^ ]*\).*#Index \1\n===============================================#g' patches/wiki_page_categories_20090520.patch
patch -p0 < patches/wiki_page_categories_20090520.patch
rake db:migrate

Après toutes ces modifs, on se rend compte que certains patches ne sont pas bien passés :

find . -iname "*.rej"

Normalement avec cet ordre de passage des patches, il n’y a que lib/redcloth3.rb dont on résoud les conflits à la main.

Voilà, hormis le système de tagging on a un wiki fonctionnel. Je ferai un nouveau post si je trouve quelque chose pour le support des tags. Il ne reste plus qu’à le remplir maintenant !

EDIT
- pour que l’activité du projet prenne en compte les changements du wiki par défaut : éditer lib/redmine.rb, et modifier autour de la ligne 155/156:

-  activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
+  activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => true

- la même option permet que les recherches prennent en compte les entrées du wiki sur le projet courant
- pour cacher la liste déroulante de sélection des projets dans la recherche : édition de public/themes/wiki/stylesheets/application.css, ajout de :

select#scope { display:none; }

EDIT2
J’ai changé l’ordre de passage des patches pour avoir le moins de choses possibles à résoudre à la main.

J’apprendrai la prochaine fois à tourner 7 fois ma langue dans ma bouche avant de dire une bêtise ; non, le nombre de /dev/ram* ce n’est pas le nombre de barettes de RAM sur un Linux. Pour connaitre ce genre d’info, lshw ou dmidecode sont plus adaptés. En reformattant la sortie avec ruby, on obtient quelque chose de ce genre :

sudo dmidecode | ruby -ne '( a=[]; 12.times{a << gets.scan(/(?:Size|Speed|Type):\s*(.*)/).first }; puts a.compact.join("/") ) if $_.match /Memory Device$/' | uniq -c

Par charité, je vous fais la même en lisible :

sudo dmidecode | \
  ruby -ne '\
    ( a=[]; \
      12.times {
        a << gets.scan(/(?:Size|Speed|Type):\s*(.*)/).first \
      }; \
      puts a.compact.join("/") \
    ) if $_.match /Memory Device$/\
  ' | uniq -c

Sur mon NC10:

      1 2048 MB/DDR2/533 MHz (1.9 ns)
      1 No Module Installed/DDR2/533 MHz (1.9 ns)

Et sur un serveur du boulot:

      8 4096 MB/<OUT OF SPEC>/667 MHz (1.5 ns)
     24 No Module Installed/<OUT OF SPEC>/Unknown

(3615 Jmelapète)

Hope this helps..

ActiveRecord::Base.logger = Logger.new(STDOUT)

(thanks)

Comments: 0 (view/add your own) Tags: rails, ruby, tech

Here we go :

gem install tzinfo builder memcache-client rack rack-test rack-mount erubis mail text-format thor bundler i18n
gem install rails --pre

Let’s give it a try ;)

Comments: 0 (view/add your own) Tags: rails, ruby, tech

On me titille de part et d’autre : Nicolas, Linux Mag (deux fois en 6 mois!), Damien et Damien, et des discussions par-ci par-là.

Donc c’est parti, j’essaie de me faire une appli sur CouchDB. Avec plein d’arrière pensées :

  • mieux maitriser l’outil et les concepts sous-jacents pour mieux comprendre (et pourquoi pas contribuer à) Chef
  • comparer ça à MongoDB (voir le railscast) ; si je bute trop, j’essaierai peut-être Mongo
  • en finir avec le gouffre conceptuel objet/relationnel ; j’espère que ça sera concluant de ce côté :)

Je me fais une petite base de connaissances parce que le besoin commence à être vraiment trop criant. J’ai des tonnes d’items non-lus et/ou à garder dans mon reader RSS, et j’ai vraiment besoin d’un outil pour organiser ça sous forme de tags (et si possible garder des copies locales des articles).

Pour simplifier le tout, je commence à partir sur du Rails 3 :

echo 'gem "couchrest"' >> Gemfile
bundle install

Des nouvelles dans les prochaines semaines, le projet sera comme d’hab’ sur github

Je travaille en ce moment sur des plugins Engines pour Redmine. Ces plugins me serviront au boulot, et permettront de laisser une situation un peu plus propre que les bidouillages actuels à mon départ. En particulier en avançant sur le plugin de gestion d’un datacenter (site et dépôt github), j’apprends énormément de choses sur le fonctionnement de Rails/Redmine/Engines, et j’entame donc une série d’articles sur ces découvertes. Ces articles supposent d’avoir déjà lu le tutoriel du site, et je repartirai souvent de cet exemple.

Cela donnera certainement lieu à des entrées dans le wiki redmine.org ou des suggestions dans les tickets. Et puis ça m’astreindra à publier un peu, comme le fait Eric dans ses Daily Refactor du core Redmine depuis 3 semaines et pour les mêmes raisons, et aussi suite à cet article de Damien.

Allons-y.

init.rb : on y ajoute la clé et la valeur par défaut du paramètre qu’on veut introduire, par exemple ici “boolean_parameter”. On précise également un partial qui permettra de gérer les paramètres du plugin :

  settings :default => { :boolean_parameter => true },
           :partial => 'settings/my_plugin'

app/views/settings/_my_plugin.html.erb : on place ici un formulaire pour gérer nos paramètres. Il sera automatiquement accessible dans la partie Administration > Plugins > lien “Configurer” sur votre plugin. “plugin_my_plugin” est à remplacer dans ce qui suit par “plugin_[nom de votre plugin]” :

<p>
  <label>Paramètre booléen</label> 
  <%= check_box_tag 'settings[boolean_parameter]', 1,
                    Setting[:plugin_my_plugin][:boolean_parameter] %>
</p>

Et voilà ! Ensuite, n’importe où dans votre plugin, vous pourrez utiliser :

Setting[:plugin_my_plugin][:boolean_parameter]

En réalité en mettant “1” comme deuxième paramètre, vous ne stockerez pas un booléen, mais “1” (coché) ou “nil” (décoché). Si vous souhaitez obtenir “true” ou “false” absolument, vous pouvez utiliser :

!!Setting[:plugin_my_plugin][:boolean_parameter]

A voir en vrai ici

Dans 4 jours, je vais me faire massacrer tenter un examen interne au boulot, censé valider mes aptitudes de “programmeur système”. Cet examen comporte une épreuve de développement (sur papier, faut pas déconner), que la plupart des gens vont passer en Java ou C/C++. Mais très peu pour moi, j’attaque en Ruby !

Je lis donc des bouquins, qui présentent un squelette de shell en C. Et en Ruby ? Eh bien en fait, contre toute attente, c’est super simple, voici un squelette fonctionnel :

#!/usr/bin/ruby

require 'readline'
require 'open3'

class Rshell
  def initialize
    loop do
      cmd = Readline.readline("$ ")
      exit if cmd.nil? or cmd == "exit"
      Open3.popen3(cmd) do |stdin,stdout,stderr|
        STDOUT.print stdout.read
        STDERR.print stderr.read
      end
    end
  end
end

Rshell.new

Il ne faut pas s’attendre à des miracles, aucun builtin, pas de gestion du PATH, mais on peut passer des commandes, différencier éventuellement STDOUT/STDERR pour le futur.

Ca me donne l’occasion de parler d’un vrai shell en Ruby, utilisé chez Heroku, Rush. J’en reparlerai dès que j’aurai testé ça au quotidien au boulot :)

Que j’aurais pu aussi sobrement appeler :

  • refaisons le match
  • massacre à la tronçonneuse
  • f*ck
require 'find'
require 'yaml'
require 'digest/md5'

class EvaFile
  attr_accessor :path

  def initialize(path)
    @path = path
  end
  
  def infos
    @infos ||= {:size => File.size(@path).tap{|s| def s.to_s; "#{self.dup} bytes"; end },
                :last_modified => File.mtime(@path),
                :md5_sum => Digest::MD5.hexdigest(File.read(@path))}
  end
end

class EvaDir
  def initialize(subdir)
    raise "Give me a (sub)directory !" if subdir.nil? || !File.directory?(subdir)
    @subdir = subdir
  end
  
  def files
    @files ||= Find.find(@subdir) do |f|
                 Find.prune if File.directory?(f)
                 EvaFile.new(f) if File.file?(f)
               end.compact
  end

  def write_info_file(filename)
    path = File.join(@subdir,filename)
    begin
      info = File.open(path,"w")
    rescue
      $stderr.puts "Error opening file #{path} for writing..."
    end
    info.write "Size: #{size} bytes\n"
    info.write "Files: #{nb_files}\n"
    files_hash = {}
    files.each do |file|
      files_hash.merge!(File.basename(file.path) => file.infos)
    end
    info.write files_hash.to_yaml
    info.close_write
  end

  def size
    @size = files.inject(0) do |memo, f|
              memo + f.infos[:size]
            end
  end
  
  def nb_files
    files.length
  end
end

class EvaUtil
  def initialize(dir)
    raise "Give me a directory !" if dir.nil? || !File.directory?(dir)
    @dir = dir
  end
  
  def subdirs
    return @subdirs if @subdirs
    @subdirs = [ EvaDir.new(@dir) ]
    @subdirs << Find.find(@dir) do |f|
                  EvaDir.new(f) if File.directory?(f)
                end
    @subdirs = @subdirs.compact.uniq
  end

  def generate_info_files(filename="infos.txt")
    @subdirs.each do |s|
      s.write_info_file(filename)
    end
  end
  
  def generate_meta_info_file(filename="metainfos.txt")
    path = File.join(@dir,filename)
    begin
      meta = File.open(path,"w")
    rescue
      $stderr.puts "Error opening file #{path} for writing..."
    end
    meta.write "Total size: #{size} bytes"
    meta.write "Total number of files: #{nb_files}"
    meta.write "Last modified (<24h) :\n #{last_modified.join("\n  ")}" if last_modified.any?
    meta.close_write
  end
  
  def last_modified
    @subdirs.inject([]) do |memo, subdir|
      memo << subdir.files.select{|f| File.mtime(f) < Time.at(Time.now.to_i - 86400)}.map(&:path)
      memo.flatten
      memo
    end
  end

  private
  def method_missing(symbol, *args)
    if %w(nb_files size).include?(symbol.to_s)
      @subdirs.inject(0) do |memo, subdir|
         memo + subdir.send(symbol)
      end
    else
      super
    end
  end
end

e = EvaUtil.new(ARGV[0])
e.generate_info_files
e.generate_meta_info_file

Garanti 100% non testé, 100% fait sans l’API, et surtout 100% fait avec un éditeur de texte. C’est sûrement bourré de conneries, mais au moins avec un truc comme ça j’aurais pas eu honte. Cela dit vue la longueur, je commence à me pardonner d’avoir barbouillé ma copie de blanco, c’était infaisable sans ça. Coder sur papier est définitivement un cauchemard. On se la refait dans 2 ans ;-)