Suivre les modifications d’un répertoire

21 05 2009

Voici un script Python pour connaître les modifications subies par un répertoire entre un instant A et un instant B. Comble du bonheur, ce n’est pas un programme en ligne de commande.

Ce programme comble (enfin je crois) un manque en la matière car des commandes existent qui font plus ou moins ça :

  • diff : permet de comparer deux répertoires, mais il ne permet pas de comparer un répertoire avec lui-même…
  • dnotify : permet de suivre les modifications en temps réel et d’exécuter un action quand une modification a lieu.  S’il n y a pas de modification, dnotify ne rend pas la main…

Pré-requis

Le programme utilise zenity et yelp qui devraient être installés par défaut sur Ubuntu (Gnome).

Installation

Télécharger le script.

Copier le script dans votre répertoire ~/bin. Il peut porter le nom que vous voulez, moi je l’ai appelé “Répertoire avant-après”, n’ayons pas peur des accents et des espaces !

Donnez-lui les droits en exécution :

Donner les droits en exécution au script

Donner les droits en exécution au script

Il est ensuite possible de créer un lanceur sur le bureau. Sous Nautilus, faites apparaître le menu contextuel en cliquant avec le bouton de droite de la souris sur le bureau :

Menu contextuel de Nautilus

Menu contextuel de Nautilus

Cliquer sur Créer un lanceur…, la fenêtre suivante apparaît :

Créer un lanceur sur le bureau

Créer un lanceur sur le bureau

Choisissez Application comme type de lanceur, donnez le nom que vous voulez et sélectionnez le script avec le bouton Parcours…. Il est également possible de changer l’icône en cliquant dessus.

Utilisation

Ce programme est très simple d’utilisation :

  • vous sélectionnez un ou plusieurs répertoires, un cliché est automatiquement pris,
  • vous exécutez les tâches dont vous voulez connaitre le comportement,
  • vous cliquez sur le bouton Valider,
  • vous lisez le résultat.

Le cliché inclut tous les sous-répertoires qu’il peut trouver.

Le programme est très rapide car les fichiers ne sont pas lus, seules les informations accessibles par la commande ls sont comparées.

Note : Il peut aussi être appelé depuis la ligne de commande en donnant les répertoires en paramètres, cela évite de devoir passer par la fenêtre de sélection. Le reste est identique.

Exemple

Dans cet exemple, je vais suivre les modifications de mon répertoire .mozilla lorsque je lance Firefox 3.

Avant de lancer Firefox, je lance le programme. La fenêtre de sélection de répertoire apparaît. Je sélectionne le répertoire et clique sur Valider.

Sélection du répertoire

Sélection du répertoire

Le premier cliché est réalisé et le message suivant apparaît :

Premier cliché réalisé

Premier cliché réalisé

Je lance donc Firefox comme prévu. Une fois Firefox lancé, je clique sur Valider. La fenêtre des résultats s’affiche alors :

Fenêtre des résultats

Fenêtre des résultats

Et voilà !

Fonctionnement

Sélection de répertoire

La sélection du ou des répertoires se fait avec zenity :

zenity --file-selection --directory --multiple

Explications :

  • –file-selection : fait apparaître la fenêtre de sélection de fichiers,
  • –directory : fonctionne en mode sélection de répertoire
  • –multiple : permet de sélectionner plusieurs répertoires

En mode sélection multiple, zenity retourne une ligne contenant les répertoires séparés par un |.

Prise de cliché

Le programme utilise la commande ls pour récupérer les répertoires, les fichiers et leur état à un instant donné :

ls -AlRZ --time-style=long-iso repertoire | grep -v -e "^total" -e "^$"

Description :

  • ls :

    • -A : affiche tous les fichiers, même cachés à l’exception de . et ..,
    • -l : affichage complet,
    • -R : analyse récursive,
    • -Z : récupère le contexte SELinux.
    • repertoire : répertoire(s) à analyser
  • grep :
    • -v : inverse le filtrage de grep,
    • -e « ^total » : élimine les lignes de totaux,
    • -e « ^$ » : élimine les lignes vides.

Toutes ces informations sont enregistrées à chaque prise de cliché pour ensuite être analysées.

La toute première lettre des droits fournis par ls -l permet de connaître le type de fichier :

  • d : répertoire,
  • : fichier standard,
  • b : fichier bloc spécial (device),
  • c : fichier caractère spécial (device),
  • l : lien symbolique,
  • p : tube nommé,
  • s : socket.

Analyse des différences

Les clichés sont indexés avec le chemin absolu de chaque fichier et répertoire.

L’algorithme d’analyse est plutôt simple :

  • ajout : on regarde les entrées présentes dans le second cliché mais pas dans le premier,
  • suppression : on regarde les entrées présentes dans le premier cliché mais pas dans le second,
  • modification : on regarde si les informations de l’entrée ont changé entre les 2 clichés (type d’entrée, propriétaire, groupe, contexte SELinux, taille, jour et heure de modification).

Affichage des résultats

Pour afficher les résultats, on génère un bête fichier HTML que yelp se chargera d’afficher. Yelp est normalement l’afficheur d’aide en ligne de Gnome mais il peut afficher n’importe quel fichier HTML (sans trop pousser non plus…) que vous lui fournissez.

Idéal pour notre cas.

Code source

Oui, le code source est lourd et mal commenté (quick and dirty)…

#!/usr/bin/env python
# coding=utf8
import sys
from subprocess import call,Popen,PIPE
from os.path import abspath,join,dirname,basename
from tempfile import NamedTemporaryFile

def decoupe_entree(ligne):
  elements=ligne.split(None,8)
  entree={
    "droits"      :elements[0],
    "proprietaire":elements[2],
    "groupe"      :elements[3],
    "contexte"    :elements[4],
    "taille"      :elements[5],
    "jour"        :elements[6],
    "heure"       :elements[7],
    "nom"         :elements[8]
  }

  lettre_type=elements[0][0]
  entree["type"]="fichier de type inconnu"
  if lettre_type=="d": entree["type"]="répertoire"
  if lettre_type=="-": entree["type"]="fichier"
  if lettre_type=="b": entree["type"]="fichier bloc spécial"
  if lettre_type=="c": entree["type"]="fichier caractère spécial"
  if lettre_type=="l": entree["type"]="lien symbolique"
  if lettre_type=="p": entree["type"]="tube nommé"
  if lettre_type=="s": entree["type"]="socket"

  return entree

def exec_analyse(repertoires):
  cmd1=['ls','-AlRZ','--time-style=long-iso']+repertoires
  cmd2=['grep','-v','-e','^total','-e','^$']

  p1=Popen(cmd1,stdout=PIPE,stderr=PIPE)
  p2=Popen(cmd2,stdin=p1.stdout,stdout=PIPE,stderr=None)

  lignes=p2.communicate()[0].splitlines()

  repcourant=""
  entrees   ={}
  for ligne in lignes:
    if ligne.startswith("/"):
      repcourant=ligne[0:-1]
    else:
      entree=decoupe_entree(ligne)
      entrees[join(repcourant,entree['nom'])]=entree

  return entrees

def compare_entree(avant,apres):
  differences=[]
  comparaisons={
    "type"        : "le type a changé",
    "proprietaire": "le propriétaire a changé",
    "groupe"      : "le groupe a changé",
    "contexte"    : "le contexte SELinux a changé",
    "taille"      : "la taille a changé",
    "jour"        : "la date de modification a changé",
    "heure"       : "l’heure de modification a changé"
  }

  for composant in comparaisons:
    if avant[composant]!=apres[composant]:
      differences.append(comparaisons[composant]+" (%s → %s)"%(avant[composant],apres[composant]))

  return differences

def compare_entrees(entrees_avant,entrees_apres):
  ajouts       =[]
  suppressions =[]
  modifications=[]

  # Recherche les suppressions et les modifications
  for entree in entrees_avant:
    if entree not in entrees_apres:
      # Entrée supprimée
      suppressions.append(entree)
      continue

    if entrees_avant[entree]!=entrees_apres[entree]:
      # Entrées différentes
      modifications.append(entree)

  # Recherche les ajouts:
  for entree in entrees_apres:
    if entree not in entrees_avant:
      # Nouvelle entrée
      ajouts.append(entree)

  return [ajouts,suppressions,modifications]

def prepare_nom(chemin):
  chemin=chemin.replace("&","&amp;").replace(">","&gt;").replace("<","&tt;")
  return dirname(chemin)+"/<strong>"+basename(chemin)+"</strong>"

def affiche_differences(entrees_avant,entrees_apres,ajouts,suppressions,modifications):
  html ='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">\n'
  html+='<html lang="fr">\n'

  html+='<head>\n'
  html+='<title>Suivi des modifications</title>\n'
  html+='<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'
  html+='</head>\n'

  html+='<body>\n'
  html+='<h1>Suivi des modifications</h1>'

  html+='<h2>Ajouts</h2>\n'
  if len(ajouts)==0:
    html+='<p>Aucun ajout</p>\n'
  else:
    html+='<ul>\n'
    for ajout in ajouts:
      html+='<li>'+prepare_nom(ajout)+' (<i>'+entrees_apres[ajout]["type"]+'</i>)</li>\n'
    html+='</ul>\n'

  html+='<h2>Suppressions</h2>\n'
  if len(suppressions)==0:
    html+='<p>Aucune suppression</p>\n'
  else:
    html+='<ul>\n'
    for suppression in suppressions:
      html+='<li>'+prepare_nom(suppression)+' (<i>'+entrees_avant[suppression]["type"]+'</i>)</li>\n'
    html+='</ul>\n'

  html+='<h2>Modifications</h2>\n'
  if len(modifications)==0:
    html+='<p>Aucune modification</p>\n'
  else:
    html+='<ul>\n'
    for modification in modifications:
      html+='<li>'+prepare_nom(modification)
      diffs=compare_entree(entrees_avant[modification],entrees_apres[modification])
      html+='<ul>\n'
      for diff in diffs:
        html+='<li>'+diff+'</li>\n'
      html+="</ul>\n"
      html+='</li>\n'
    html+='</ul>\n'

  html+='</body>\n'
  html+='</html>'

  return html  

if len(sys.argv)>=2:
  rep_donnes=sys.argv[1:]
else:
  rep_donnes=Popen(['zenity','--file-selection','--directory','--multiple'],stdout=PIPE).communicate()[0].splitlines()[0]
  if rep_donnes=="": exit(0)
  rep_donnes=rep_donnes.split('|')

repertoires=[]
for rep in rep_donnes:
  rep=rep.strip()
  if rep=="": continue
  rep=abspath(rep)
  if rep in repertoires: continue
  repertoires.append(abspath(rep))

if len(repertoires)==0: exit(0)

entrees_avant=exec_analyse(repertoires)

call(["zenity","--info","--title=Premier cliché réalisé","--text=Un premier cliché du répertoire a été réalisé.\nVeuillez cliquer sur le bouton pour lancer le second et afficher les différences"])

entrees_apres=exec_analyse(repertoires)

(ajouts,suppressions,modifications)=compare_entrees(entrees_avant,entrees_apres)

html=affiche_differences(entrees_avant,entrees_apres,ajouts,suppressions,modifications)
sortie=NamedTemporaryFile(suffix=".html")
sortie.write(html)
sortie.flush()
call(["yelp",sortie.name])

Actions

Information

10 responses

21 05 2009
Twitted by zigazou

[…] This post was Twitted by zigazou – Real-url.org […]

30 05 2009
M.i.B

Salut à toi Zigazou et merci pour ces scripts.
Je viens de tester diffrep il fonctionne bien mais j’ai un petit bug, une fois que la comparaison faite elle ne s’affiche pas dans Aide.
Visiblement j’ai un problème de droit sur le fichier situé dans tmp, après vérification seul le propriétaire a des droits et je suis le propriétaire.

Voici le messade: Le fichier « /tmp/tmpnpeufB.html » ne peut être lu. Ce fichier peut être manquant, ou vous n’avez peut-être pas les permissions de le lire.

Par contre si j’ouvre tmpnpeufB.html avec firefox pas de soucis.

30 05 2009
zigazou

Salut M.i.B !

diffrep utilise yelp pour l’affichage du fichier html. As-tu le même message si tu fais manuellement yelp /tmp/tmpnpeufB.html ?

Sinon, il est toujours possible de remplacer yelp par xdg-open (toute dernière ligne du script).

S’il continue à afficher ce message, ça veut dire que ça vient de ma gestion du fichier temporaire.

Merci d’essayer !

@+

30 05 2009
M.i.B

je viens de lancer la commande yelp /tmp/tmpTSB5Ni.html même résultat.
Après la modif du script avec xdg-open, Firefox ne peut trouver le fichier à l’adresse /tmp/tmpTSB5Ni.html
Je vais donc vérifier mon tmp pas de tmpTSB5Ni.html présent
Si ça peux t’aider.
Au fait je suis sous intrepid.

30 05 2009
zigazou

À mon avis, ne te tracasse pas trop pour ton répertoire tmp, je pense que ça vient de mon script.

En faisant des tests, j’obtiens les mêmes symptômes que toi en ajoutant un sortie.close() juste avant la dernière ligne

Si je fais un ls -l, j’obtiens ça pour l’utilisateur jojo :
-rw——- 1 jojo jojo 426 2009-05-30 18:21 /tmp/tmphOimj1.html

Peux-tu essayer en remplaçant les dernières lignes par celles-ci ?
sortie=NamedTemporaryFile(suffix= ».html »,delete=False)
sortie.write(html)
sortie.flush()
sortie.close()
call([« yelp »,sortie.name])

Merci

30 05 2009
M.i.B

en modifiant le script comme demandé, il ne se lance pas et dans le terminal ça donne ceci

mib@MYPC1:$ ./diffrep
File « ./diffrep », line 175
sortie=NamedTemporaryFile(suffix=”.html”,delete=False)
^
SyntaxError: invalid syntax

30 05 2009
zigazou

C’est dû à WordPress qui met des guillemets typographiques et non pas informatiques, quand tu fais un copier-coller, remplacer les guillemets par celles de ton clavier (sur la touche 3  » #)

30 05 2009
M.i.B

Visiblement j’ai encore caractères non reconnu
mib@MYPC1:$ ./diffrep
Traceback (most recent call last):
File « ./diffrep », line 175, in
sortie=NamedTemporaryFile(suffix= ».html »,delete=False)
TypeError: NamedTemporaryFile() got an unexpected keyword argument ‘delete’

30 05 2009
zigazou

Argh !

Intrepid doit être en Python 2.4.

Jaunty est en Python 2.6…

C’est pour cela qu’il ne prend pas le paramètre delete…

Va falloir que je planche plus pour la compatibilité avec la 2.4 (c’est peut-être même de là que vient le problème général😉 )

30 05 2009
M.i.B

Ok et merci pour toutes ces tests et explications, en espérant que tu trouves une solution pour intrepid

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s




%d blogueurs aiment cette page :