Élie Gouzien

Élie Gouzien

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Created on Fri Dec 20 11:53:35 2013

Régénère les liens vers les fichiers archives d'Unison sous Windows.
Optionnellement il détruit les vieux liens.
Détecte les fichiers recréés par Unison et le replace dans le dossier Windows.

Ce script devrait être appelé après chaque modification d'un profil.
Après toute utilisation d'Unison sur Linux, il doit être appelé avant
d'utiliser Unison sous Windows. Typiquement il est exécuté automatiquement
après (voir aussi avant) chaque exécution d'Unison sous Linux.
Voir le fichier `unison` associé.

Attention, s'il y a deux rootalias dans le fichier, il prend juste en compte
le dernier. Soyez prudent avant d'utiliser deux rootalias dans un fichier de
configuration.

@author: Élie Gouzien (une recherche sur Internet vous permet de me contacter)
"""

import os
import codecs
import hashlib
import shutil
import argparse

# UNISON_WIN : dossier Unison pour windows
UNISON_WIN = os.path.normpath("/mnt/OS/Users/Elie/.unison/")
# UNISON_LINUX : dossier Unison pour linux
UNISON_LINUX = os.path.normpath("/home/elie/.unison/")

# OPTIONS_RECOMMANDEES : dictionnaire contenant les options qu’il serait sage
# de placer dans le fichier de profil afin de désactiver les options sous
# Linux qui ne peuvent être activées sous Windows (permissions, casse).
# Cela n'affecte pas le comportement du programme mais affiche un message.
OPTIONS_RECOMMANDEES = {'fat': 'true'}


parser = argparse.ArgumentParser(description="Crée les liens pour les fichiers\
                                 d'archive d'Unison.", epilog="Lire le code \
                                 source pour plus de détails.")
parser.add_argument('-c', '--clean', dest='recreer', action='store_true',
                    help="Détruit les liens avant de les recréer")
args = parser.parse_args()


def compter(liste, clef, valeur):
    """Compte le nombre de fois que liste[i][clef] == valeur.

    liste : liste de dictionnaire
    clef : clef de dictionnaire, chaîne
    valeur : valeur de dictionnaire, chaîne
    """
    nombre = 0
    for dico in liste:
        if dico[clef] == valeur:
            nombre += 1
    return nombre


def indice_convenant(liste, clef, valeur):
    """Donne le premier indice i tel que liste[i][clef] == valeur.

    liste : liste de dictionnaire
    clef : clef de dictionnaire, chaîne
    valeur : valeur de dictionnaire, chaîne
    """
    indice = 0
    while True:
        if liste[indice][clef] == valeur:
            return indice
        indice += 1


def texte_erreur(liste, profile, chemin_local):
    """Renvoie une description d'erreur contenant les caractéristiques des
    fichiers d'archive gérant 'chemin_local'.

    liste (list): liste de l'index des fichiers d'archive (ARCHIVES)
    chemin_local (str): le chemin (adresse) du profil dont on veut afficher
        les archives correspondantes.
    """
    sortie = "Profile " + profile + " :\n"
    sortie += "Attention, il y a plusieurs fichiers d'archive \
associables au profil.\n"
    for dico in liste:
        if dico['chemin_local'] == chemin_local:
            sortie += "Fichier d'archive " + dico['ar_win'] + " :\n"
            sortie += "\tVersion archive : " + str(dico['version']) + "\n"
            sortie += "\tChemin local (root) : " + dico['chemin_local'] + "\n"
            sortie += ("\tChemins canoniques (roots) :"
                       + dico['chemins_profile'] + "\n")
            sortie += ("\tFichier annexe (si existant) :" + dico['fp_win']
                       + "\n")
    sortie += "Le plus simple est souvent de supprimer les vieux fichiers."
    return sortie


# Suppression des anciens liens.
if args.recreer is True:
    for fichier in os.listdir(UNISON_LINUX):
        fichier = os.path.join(UNISON_LINUX, fichier)
        if os.path.islink(fichier):
            os.remove(fichier)

# Création d'une sorte d'index des fichiers d'archives.
# TIP : c'est le seul endroit où on peut trouver les noms canoniques sans se
# connecter à l'ordinateur distant.
# HINT: Le programme nécessite d'avoir synchronisé au moins une fois chaque
# profil depuis Windows.
ARCHIVES = []
for fichier in os.listdir(UNISON_WIN):
    chemin_fichier = os.path.join(UNISON_WIN, fichier)
    if not (os.path.isfile(chemin_fichier) and
            os.path.splitext(fichier)[1] == "" and fichier.startswith("ar")):
        continue
    archive = {}
    # HINT : il faut utiliser l'encodage de Windows.
    with codecs.open(chemin_fichier, 'r', 'iso-8859-15') as source:
        archive['version'] = source.readline().split()[-1]
        archive['chemin_local'], _, archive['chemins_profile'] = \
            source.readline().strip('\n').partition(" synchronizing roots ")
    archive['chemin_local'] = \
        archive['chemin_local'].partition("Archive for root ")[2]
    # TODO: controler l'encodage utilisé par unison
    archive_id = (archive['chemin_local'] + ';' + archive['chemins_profile']
                  + ';' + archive['version']).encode('ascii')
    archive['somme_controle'] = hashlib.md5(archive_id).hexdigest()
    archive['ar_win'] = chemin_fichier
    archive['fp_win'] = os.path.join(UNISON_WIN,
                                     'fp' + archive['somme_controle'][0:6])
    archive['ar_linux'] = os.path.join(UNISON_LINUX,
                                       'ar' + archive['somme_controle'])
    archive['fp_linux'] = os.path.join(UNISON_LINUX,
                                       'fp' + archive['somme_controle'])
    archive['version'] = int(archive['version'])
    assert os.path.basename(archive['ar_linux']).startswith(
        os.path.basename(archive['ar_win'])), "Problème de nomage."
    ARCHIVES.append(archive)

# Boucle sur les profils pour trouver quelles archives il faut créer le lien.
for fichier in os.listdir(UNISON_LINUX):
    chemin_fichier = os.path.join(UNISON_LINUX, fichier)
    if not (os.path.isfile(chemin_fichier) and
            os.path.splitext(fichier)[1] == ".prf"):
        continue

    # construction de la liste d'options du profil.
    # TODO : le type dictionnaire n'est pas adapté ici, car on peut utiliser
    #        plusieurs fois la même option.
    options = {}
    with codecs.open(chemin_fichier, 'r', 'iso-8859-15') as source:
        for ligne in source:
            ligne = ligne.strip('\n').strip()
            if ligne.find('=') != -1:
                clef, _, valeur = ligne.partition('=')
                clef = clef.strip()
                valeur = valeur.strip()
                options[clef] = valeur

    # Traite les profils où c'est nécessaire
    if 'rootalias' in options:
        chemin_local = options['rootalias'].partition('->')[2].strip()
        nombre_convenant = compter(ARCHIVES, 'chemin_local', chemin_local)
        if nombre_convenant == 0:
            print("Profile " + fichier + " :")
            print("\tAttention, il n'y a pas d'archive associée au profil. "
                  "vous devriez utiliser Unison avec Windows au moins une "
                  "fois.")
            continue
        elif nombre_convenant > 1:
            raise Exception(texte_erreur(ARCHIVES, fichier, chemin_local))

        # Cœur du programme
        i = indice_convenant(ARCHIVES, 'chemin_local', chemin_local)
        if not os.path.islink(ARCHIVES[i]['ar_linux']):
            try:
                os.symlink(ARCHIVES[i]['ar_win'], ARCHIVES[i]['ar_linux'])
            except OSError as erreur:
                if erreur.errno != 17:
                    raise
                shutil.copyfile(ARCHIVES[i]['ar_linux'], ARCHIVES[i]['ar_win'])
                os.remove(ARCHIVES[i]['ar_linux'])
                os.symlink(ARCHIVES[i]['ar_win'], ARCHIVES[i]['ar_linux'])
        if not os.path.islink(ARCHIVES[i]['fp_linux']):
            try:
                os.symlink(ARCHIVES[i]['fp_win'], ARCHIVES[i]['fp_linux'])
            except OSError as erreur:
                if erreur.errno != 17:
                    raise
                shutil.copyfile(ARCHIVES[i]['fp_linux'], ARCHIVES[i]['fp_win'])
                os.remove(ARCHIVES[i]['fp_linux'])
                os.symlink(ARCHIVES[i]['fp_win'], ARCHIVES[i]['fp_linux'])

        # Préviens si les options ne sont pas bonnes
        if not all(options.get(key) == val
                   for key, val in OPTIONS_RECOMMANDEES.items()):
            print("Profile " + fichier + " :")
            print("Attention, afin de ne pas générer de conflits, nous "
                  "recommandons d'appliquer les mêmes options que sous "
                  "Windows :")
            print(OPTIONS_RECOMMANDEES)
        # Crée le lien pour le fichier log
        try:
            fichier_log = os.path.basename(options['logfile'])
            if not os.path.islink(os.path.join(UNISON_LINUX, fichier_log)):
                os.symlink(os.path.join(UNISON_WIN, fichier_log),
                           os.path.join(UNISON_LINUX, fichier_log))
            del fichier_log
        except KeyError:
            pass