IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez Python Discussion :

Une sauvegarde incrémentale en Python!


Sujet :

Contribuez Python

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Expert confirmé
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 486
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 486
    Billets dans le blog
    6
    Par défaut Une sauvegarde incrémentale en Python!
    Bonjour,

    Le principe d'une sauvegarde est simple: disposer de 2 copies des fichiers importants de sorte qu'une panne ou une maladresse, un virus, un piratage (ransonware?), etc... qui ferait disparaître l'une des copies nous laisserait l'autre intacte. Il est bien sûr recommandé que les 2 copies ne soient pas sur le même support (disque dur)! Le mieux est même que la sauvegarde soit sur un disque amovible (USB par exemple) débranché en dehors des sauvegardes (dans un coffre ignifugée?), ou même hors du bâtiment (liaison FTP sur Internet?). Tout dépend, bien sûr, de l'importance des fichiers, de celle des risques et des moyens disponibles.

    On va ici se restreindre à un contexte simple de PC et de fichiers utilisateurs courants: courriers, images, vidéos, sources de programmes développés, etc... Les sauvegardes sont importantes, mais souvent négligées par les amateurs: je me rappelle que dans mon photo-club, certains de ceux qui s'intéressaient aux sauvegardes avaient déjà tout perdu!

    Le problème qu'on va résoudre ici est celui-ci: faire une sauvegarde d'un gros répertoire en copiant tout est très long! Un répertoire de plusieurs centaines de Go peut demander plusieurs heures. Pour résoudre cela, il suffit à chaque sauvegarde de se contenter de ne copier que les fichiers absents ou plus récents de la sauvegarde précédente! Cela fera passer la sauvegarde de plusieurs heures à quelques minutes ou quelques secondes. C'est ce qu'on appellera ici: sauvegarde incrémentale (ou incrémentielle).

    Il existe déjà de nombreuses solutions existantes pour faire ça. Sous Windows, par exemple, il existe en console "xcopy" et "robocopy" qui marchent très bien mais qui ne sont pas toujours facile à configurer. Toujours sous Windows, un logiciel gratuit comme "SyncBackFree" (https://www.2brightsparks.com/freewa...eware-hub.html) fait ça très bien!

    Mais comme c'est amusant à développer soi-même, on ne va pas s'en priver! Le grand avantage, c'est qu'on pourra le personnaliser comme on veut. Quand à la rapidité de la sauvegarde en Python, ce n'est pas un problème, puisque le gros du délai sera consommé par les opérations physiques de lecture-écriture qui utilisent les fonctions de base de l'OS.

    Le principe du logiciel proposé est celui-ci:

    - on lit le répertoire source, on prend tous ses fichiers un par un, on recalcule pour chacun d'eux son chemin sur le répertoire destination, et on teste: si le fichier destination n'existe pas ou s'il est antérieur au fichier source, on le recopie. il faudra, bien sûr, créer les sous-répertoires nécessaires sur la destination.

    - Il y a aussi une option "miroir": on lit le répertoire destination, on prend tous ses fichiers un par un, on recalcule pour chacun d'eux son chemin sur le répertoire source, et on teste: si le fichier source n'existe pas, on le supprime du répertoire destination. Pareil pour les sous-répertoires.

    Pour explorer le contenu d'un répertoire, il existe sous Python plusieurs solutions: os.listdir, os.scandir, glob.glob, os.walk. Après plusieurs essais, j'ai choisi ici: os.walk, parce qu’il a au moins deux gros avantages:
    - gérer convenablement les erreurs. Il est en effet désagréable de voir un logiciel comme ça se planter sur un des fichiers en erreur en ne traitant pas les suivants, ou pire, ne pas traiter les fichiers en erreur sans rien dire.
    - permettre à certains sous-répertoires d'être éliminés des explorations suivantes, ce qui évite d'avoir à traiter ses fichiers (ici pour l'option miroir).
    Il existe un autre avantage d'os.walk qu'on n'utilisera pas ici: pouvoir explorer les fichiers de l'arborescence d'un répertoire en commençant par les feuilles et en remontant jusqu'au tronc (option "topdown=False", donc un parcours "bottom up").

    Voilà le code principal (largement commenté):

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    #############################################################################
    def copieincrementale(rep1, rep2, miroir=False, fnerreur=None, recursion=True, 
                          suiviliens=False):
        """Fait une sauvegarde incrémentale de rep1 sur rep2, c'est à dire ne 
           copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
           - rep1: le répertoire à sauvegarder
           - rep2: le répertoire qui recevra les copies de rep1
           - miroir: si True: supprime fichiers et répert. de rep2 absents de rep1
           - fnerreur: si != None: fonction callback pour le message d'erreur
           - recursion: si True, traite aussi les sous-répertoires
           - suiviliens: si True, suit les liens symboliques        
        """
        #=========================================================================
        # calcule les adresses absolues des répertoires donnés
        rep1 = os.path.abspath(os.path.expanduser(rep1))
        rep2 = os.path.abspath(os.path.expanduser(rep2))
     
        #=========================================================================
        # vérifie l'accès à rep1
        if not os.access(rep1, os.X_OK):
            if fnerreur is not None: fnerreur("Erreur accès à rep1")
            return # impossible de rentrer dans rep1
     
        #=========================================================================
        # vérifie l'existence de rep2
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.mkdir(rep2)
            except Exception as msgerr:
                if fnerreur is not None: fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie l'accès
            if not os.access(rep2, os.X_OK):
                if fnerreur is not None: fnerreur("Erreur accès à rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # fait la copie incrémentale 
        lgrep1, lgrep2 = len(rep1), len(rep2) # nb de caractères de rep1 et rep2
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur, followlinks=suiviliens):
            for fic in fics:
                nfc1 = os.path.join(repert, fic) # fic avec son chemin sur rep1
                nfc2 = os.path.join(rep2, repert[lgrep1+1:], fic)# avec chemin sur rep2
                if not os.path.exists(nfc2):
                    # crée le chemin nécessaire pour copier le nouveau fichier
                    chemin = os.path.dirname(nfc2)
                    if not os.path.exists(chemin):
                        try:
                            os.makedirs(chemin)
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
                            continue # échec => passer au fic suivant
                    # copie le nouveau fichier
                    try:
                        copy2(nfc1, nfc2)
                        print("Copie le nouveau fichier: {}".format(nfc1))
                    except Exception as msgerr:
                        if fnerreur is not None: fnerreur(msgerr)    
                else:
                    # le fichier fic existe déjà sur rep2: on compare les dates
                    try:
                        if os.path.getmtime(nfc1) > os.path.getmtime(nfc2):
                            # copie le fichier fic de rep1 plus récent que celui de rep2
                            copy2(nfc1, nfc2) 
                            print("Copie le fichier plus récent: {}".format(nfc1))
                    except Exception as msgerr:
                        # erreur possible avec getmtime ou copy2
                        if fnerreur is not None: fnerreur(msgerr)               
     
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # traite l'effet miroir
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur, followlinks=suiviliens):
                # supprime les fichiers de rep2 absents de rep1
                for fic in fics:
                    nfc2 = os.path.join(repert, fic)
                    nfc1 = os.path.join(rep1, repert[lgrep2+1:], fic)
                    if not os.path.exists(nfc1):
                        # le fichier fic de rep2 absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            print("Miroir => supprime le fichier: {}".format(nfc2))
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
     
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                    nrc2 = os.path.join(repert, reps[i]) # rep avec chemin sur rep2
                    nrc1 = os.path.join(rep1, repert[lgrep2+1:], reps[i]) # rep avec chemin sur rep1
                    if not os.path.exists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            rmtree(nrc2)
                            print("Miroir => supprime le répertoire: {}".format(nrc2))
                            reps.pop(i) # on retire rep[i] de l'exploration suivante
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
    Pour exploiter cette fonction dans une console, on va utiliser le module "argparse", qui permettra de recevoir les arguments passés en ligne de commande sans se tromper. Voilà la partie "argparse":

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #############################################################################
    if __name__ == "__main__":
     
        #========================================================================
        # récupération des données de traitement de la ligne de commande
        import argparse
     
        def str2bool(v):
            """Traduit les chaines de caractères en booléens
            """
            if v.lower() in ["o", "oui", "y", "yes", "t", "true"]:
                return True
            elif v.lower() in ["n", "non", "not", "f", "false"]:
                return False
            else:
                raise Exception ("Erreur: valeur booléenne attendue pour {}".format(v))
     
        # création du parse des arguments
        parser = argparse.ArgumentParser(description="Sauvegarde incrémentale d'un répertoire")
     
        # déclaration et configuration des arguments
        parser.add_argument('-s', '--source', dest='source', type=str, required=True, action="store", help="Répertoire source")
        parser.add_argument('-d', '--destination', dest='destination', type=str, required=True, action="store", help="Répertoire destination")
        parser.add_argument('-m', '--miroir', dest="miroir", type=str2bool, choices=[True, False], default=False, help="Si True, supprime les fichiers et répertoires destination absents de la source")
        parser.add_argument('-r', '--recursion', dest="recursion", type=str2bool, choices=[True, False], default=True, help="Si True, explore les sous-répertoires")
        parser.add_argument('-l', '--suiviliens', dest="suiviliens", type=str2bool, choices=[True, False], default=False, help="Si True, suit les liens symboliques")
     
        # dictionnaire des arguments passés au lancement
        dicoargs = vars(parser.parse_args())
     
        # Récupére les données de traitement
        source = dicoargs["source"]
        destination = dicoargs["destination"]
        miroir = dicoargs["miroir"]
        recursion =  dicoargs["recursion"]
        suiviliens = dicoargs["suiviliens"]
    Un des avantages de cette partie, c'est qu'on peut rappeler la syntaxe à utiliser en faisant simplement dans la console:

    Ce qui affichera:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    usage: sauvincrem.py [-h] -s SOURCE -d DESTINATION [-m {True,False}]
                         [-r {True,False}] [-l {True,False}]
     
    Sauvegarde incrémentale d'un répertoire
     
    optional arguments:
      -h, --help            show this help message and exit
      -s SOURCE, --source SOURCE
                            Répertoire source
      -d DESTINATION, --destination DESTINATION
                            Répertoire destination
      -m {True,False}, --miroir {True,False}
                            Si True, supprime les fichiers et répertoires
                            destination absents de la source
      -r {True,False}, --recursion {True,False}
                            Si True, explore les sous-répertoires
      -l {True,False}, --suiviliens {True,False}
                            Si True, suit les liens symboliques
    Un exemple de ligne de commande pour un répertoire de courrier (rappel: les chemins des fichiers doivent avoir des guillemets quand il y a des espaces dans ces chemins):

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    sauvincrem.py -s "E:\Documents_2\Courrier Privé 2" -d "E:\Documents_2\Courrier Privé 2 2" -m True -r True -l False
    Bien sûr, la 1ère sauvegarde qu'on fait doit tout copier!

    En cas d'erreur de syntaxe dans la ligne de commande, le logiciel le signale en rappelant la syntaxe à utiliser.

    On peut aussi faire que ce qui devrait être affiché sera enregistré dans un fichier "log" en ajoutant à la ligne de commande: "> fichier.log", ou même faire que le traitement soit "silencieux" (=sans affichage) avec "> nul" (toujours sous Windows, mais il y a l’équivalent sous Linux et MacOS).

    Après la récupération des données de traitement par argparse, voilà comment on peut lancer la fonction avec ses arguments et récupérer ses résultats:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        # Sauvegarde
        erreurs = []
        secs = perf_counter()
        copieincrementale(source, destination, miroir, erreurs.append, recursion, suiviliens)
        secs = perf_counter()-secs
        #========================================================================
        # Affichage des erreurs s'il y en a
        if erreurs!=[]:
            print("Erreurs: {}".format(len(erreurs)))
            print()
            for erreur in erreurs:
                print(erreur)
            print()    
     
        #========================================================================    
        _, _, _, _, msgtps = secs2jhms(secs)
        print("Sauvegarde terminée en {}".format(msgtps))
    On voit que, pour cette dernière partie, il faut ajouter 2 fonctions de temps: une fonction qui donne la date et l'heure de l'ordinateur pour donner le moment d'exécution de la sauvegarde, et une fonction donnant le délai du traitement:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #############################################################################
    def cejour():
        """Retourne la date et l'heure de l'ordinateur: jj/mm/aaaa hh:mm:ss
        """
        dt = datetime.today()
        return "{}/{}/{} {}:{}:{}".format(dt.day, dt.month, dt.year, dt.hour, dt.minute, int(dt.second))
     
    #############################################################################
    def secs2jhms(secs):
        """Convertit le délai secs (secondes) => jours, heures, minutes, secondes
            En retour, j, h et m sont des entiers. s est de type float
                    et msgtps (présentation du délai pour affichage) est un str 
        """
        m, s = divmod(float(secs), 60)
        m = int(m)
        h, m = divmod(m, 60)
        j, h = divmod(h, 24)
        if j>0:
            msgtps = "{:d}j {:d}h {:d}m {:.6f}s".format(j, h, m, s)
        elif h>0:
            msgtps = "{:d}h {:d}m {:.6f}s".format(h, m, s)
        elif m>0:
            msgtps = "{:d}m {:.6f}s".format(m, s)
        else:
            msgtps = "{:.6f}s".format(s)            
     
        return j, h, m, s, msgtps
    On obtient ainsi, si on a utilisé l'option "miroir", un répertoire destination identique au répertoire source.

    Évolution du logiciel: sur cette base simplifiée, on peut ajouter des options comme (c'est ce que j'utilise en fait):
    - une sélection des fichiers à sauvegarder: il suffit de mettre des motifs "wildcard" en argument. Par exemple: "*.py;*.pyw".
    - une liste de sous-répertoires à exclure de la sauvegarde. Par exemple: "__pycache__".
    Mais ça complique un peu le traitement de l'option miroir!

    Rien ne vous empêche aussi de faire une version "FTP" si vous pouvez faire cette sauvegarde su un site web à vous!

    Au lieu de lancer la sauvegarde par ligne de commande, et dans la mesure où ça sera fréquent et concernera les mêmes répertoires, on peut lancer un script "batch" sous Windows (shell sous Linux et MacOS):

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    ECHO OFF
    CHCP 65001
    REM Sauvegarde incrémentale
     
    REM ==========================================================================
     
    SET scriptpy=sauvincrem.py
     
    SET source="E:\Documents_2\Courrier Privé 2"
    SET destination="E:\Documents_2\Courrier Privé 2 2"
     
    SET miroir=True
    SET recursion=True
    SET suiviliens=False
     
    REM ==========================================================================
     
    START "" /B /WAIT Python.exe ^
    %scriptpy% ^
    --source %source% ^
    --destination %destination% ^
    --miroir %miroir% ^
    --recursion %recursion% ^
    --suiviliens %suiviliens%
     
    PAUSE
    Ce qui lancera la même ligne de commande que plus haut. On n'a plus qu'à créer autant de fichiers comme ça qu'il y a de répertoires à sauvegarder:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    Sauve_Courrier.bat
    Sauve_PythonDev.bat
    Sauve_Images.bat
    Sauve_Videos.bat
    Etc...
    Rien ne vous empêche, bien sûr, de faire une version exécutable "autonome" (*.exe sous Windows) avec pyinstaller ou cx_freeze, ce qui permettra d'exécuter ce logiciel de sauvegarde sur des PC sans Python.

    Je n'ai pas testé sur Linux ni sur MacOS, mais comme je n'ai pas utilisé d'instruction spécifique à Windows, ça ne devrait pas poser de problème: n'hésitez pas à me le confirmer, ou à me signaler les difficultés rencontrées!

    Voilà la récap de la source complète:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
     
    """
    Sauvegarde incrémentale
     
    Fait une sauvegarde incrémentale de rep1 sur rep2, c'est à dire ne 
    copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
     
    Plus option miroir: efface de rep2 les fichiers et sous-répertoires absents 
    de rep1 
    """
     
    import os
    from shutil import copy2, rmtree
    from datetime import datetime # pour la date/heure de l'ordinateur
    from time import perf_counter # pour le calcul du temps de traitement
     
    #############################################################################
    def cejour():
        """Retourne la date et l'heure de l'ordinateur: jj/mm/aaaa hh:mm:ss
        """
        dt = datetime.today()
        return "{}/{}/{} {}:{}:{}".format(dt.day, dt.month, dt.year, dt.hour, dt.minute, int(dt.second))
     
    #############################################################################
    def secs2jhms(secs):
        """Convertit le délai secs (secondes) => jours, heures, minutes, secondes
            En retour, j, h et m sont des entiers. s est de type float
                    et msgtps (présentation du délai pour affichage) est un str 
        """
        m, s = divmod(float(secs), 60)
        m = int(m)
        h, m = divmod(m, 60)
        j, h = divmod(h, 24)
        if j>0:
            msgtps = "{:d}j {:d}h {:d}m {:.6f}s".format(j, h, m, s)
        elif h>0:
            msgtps = "{:d}h {:d}m {:.6f}s".format(h, m, s)
        elif m>0:
            msgtps = "{:d}m {:.6f}s".format(m, s)
        else:
            msgtps = "{:.6f}s".format(s)            
     
        return j, h, m, s, msgtps
     
    #############################################################################
    def copieincrementale(rep1, rep2, miroir=False, fnerreur=None, recursion=True, 
                          suiviliens=False):
        """Fait une sauvegarde incrémentale de rep1 sur rep2, c'est à dire ne 
           copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
           - rep1: le répertoire à sauvegarder
           - rep2: le répertoire qui recevra les copies de rep1
           - miroir: si True: supprime fichiers et répert. de rep2 absents de rep1
           - fnerreur: si != None: fonction callback pour le message d'erreur
           - recursion: si True, traite aussi les sous-répertoires
           - suiviliens: si True, suit les liens symboliques        
        """
        #=========================================================================
        # calcule les adresses absolues des répertoires donnés
        rep1 = os.path.abspath(os.path.expanduser(rep1))
        rep2 = os.path.abspath(os.path.expanduser(rep2))
     
        #=========================================================================
        # vérifie l'accès à rep1
        if not os.access(rep1, os.X_OK):
            if fnerreur is not None: fnerreur("Erreur accès à rep1")
            return # impossible de rentrer dans rep1
     
        #=========================================================================
        # vérifie l'existence de rep2
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.mkdir(rep2)
            except Exception as msgerr:
                if fnerreur is not None: fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie l'accès
            if not os.access(rep2, os.X_OK):
                if fnerreur is not None: fnerreur("Erreur accès à rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # fait la copie incrémentale 
        lgrep1, lgrep2 = len(rep1), len(rep2) # nb de caractères de rep1 et rep2
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur, followlinks=suiviliens):
            for fic in fics:
                nfc1 = os.path.join(repert, fic) # fic avec son chemin sur rep1
                nfc2 = os.path.join(rep2, repert[lgrep1+1:], fic)# avec chemin sur rep2
                if not os.path.exists(nfc2):
                    # crée le chemin nécessaire pour copier le nouveau fichier
                    chemin = os.path.dirname(nfc2)
                    if not os.path.exists(chemin):
                        try:
                            os.makedirs(chemin)
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
                            continue # échec => passer au fic suivant
                    # copie le nouveau fichier
                    try:
                        copy2(nfc1, nfc2)
                        print("Copie le nouveau fichier: {}".format(nfc1))
                    except Exception as msgerr:
                        if fnerreur is not None: fnerreur(msgerr)    
                else:
                    # le fichier fic existe déjà sur rep2: on compare les dates
                    try:
                        if os.path.getmtime(nfc1) > os.path.getmtime(nfc2):
                            # copie le fichier fic de rep1 plus récent que celui de rep2
                            copy2(nfc1, nfc2) 
                            print("Copie le fichier plus récent: {}".format(nfc1))
                    except Exception as msgerr:
                        # erreur possible avec getmtime ou copy2
                        if fnerreur is not None: fnerreur(msgerr)               
     
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # traite l'effet miroir
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur, followlinks=suiviliens):
                # supprime les fichiers de rep2 absents de rep1
                for fic in fics:
                    nfc2 = os.path.join(repert, fic)
                    nfc1 = os.path.join(rep1, repert[lgrep2+1:], fic)
                    if not os.path.exists(nfc1):
                        # le fichier fic de rep2 absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            print("Miroir => supprime le fichier: {}".format(nfc2))
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
     
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                    nrc2 = os.path.join(repert, reps[i]) # rep avec chemin sur rep2
                    nrc1 = os.path.join(rep1, repert[lgrep2+1:], reps[i]) # rep avec chemin sur rep1
                    if not os.path.exists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            rmtree(nrc2)
                            print("Miroir => supprime le répertoire: {}".format(nrc2))
                            reps.pop(i) # on retire rep[i] de l'exploration suivante
                        except Exception as msgerr:
                            if fnerreur is not None: fnerreur(msgerr)
     
    #############################################################################
    if __name__ == "__main__":
     
        #========================================================================
        # récupération des données de traitement de la ligne de commande
        import argparse
     
        def str2bool(v):
            """Traduit les chaines de caractères en booléens
            """
            if v.lower() in ["o", "oui", "y", "yes", "t", "true"]:
                return True
            elif v.lower() in ["n", "non", "not", "f", "false"]:
                return False
            else:
                raise Exception ("Erreur: valeur booléenne attendue pour {}".format(v))
     
        # création du parse des arguments
        parser = argparse.ArgumentParser(description="Sauvegarde incrémentale d'un répertoire")
     
        # déclaration et configuration des arguments
        parser.add_argument('-s', '--source', dest='source', type=str, required=True, action="store", help="Répertoire source")
        parser.add_argument('-d', '--destination', dest='destination', type=str, required=True, action="store", help="Répertoire destination")
        parser.add_argument('-m', '--miroir', dest="miroir", type=str2bool, choices=[True, False], default=False, help="Si True, supprime les fichiers et répertoires destination absents de la source")
        parser.add_argument('-r', '--recursion', dest="recursion", type=str2bool, choices=[True, False], default=True, help="Si True, explore les sous-répertoires")
        parser.add_argument('-l', '--suiviliens', dest="suiviliens", type=str2bool, choices=[True, False], default=False, help="Si True, suit les liens symboliques")
     
        # dictionnaire des arguments passés au lancement
        dicoargs = vars(parser.parse_args())
     
        # Récupére les données de traitement
        source = dicoargs["source"]
        destination = dicoargs["destination"]
        miroir = dicoargs["miroir"]
        recursion =  dicoargs["recursion"]
        suiviliens = dicoargs["suiviliens"]
     
        #========================================================================
        # en-tête d'affichage
        print("SAUVEGARDE INCREMENTALE ({})".format(cejour()))
        print()
     
        print("Répertoire source: {}".format(source))
        print("Répertoire destination: {}".format(destination))
        print("Option miroir: {}".format(miroir))
        print("Recursion: {}".format(recursion))
        print("Suivi des liens symboliques: {}".format(suiviliens))
        print()
     
        #========================================================================
        # Sauvegarde
        erreurs = []
        secs = perf_counter()
        copieincrementale(source, destination, miroir, erreurs.append, recursion, suiviliens)
        secs = perf_counter()-secs
        #========================================================================
        # Affichage des erreurs s'il y en a
        if erreurs!=[]:
            print("Erreurs: {}".format(len(erreurs)))
            print()
            for erreur in erreurs:
                print(erreur)
            print()    
     
        #========================================================================    
        _, _, _, _, msgtps = secs2jhms(secs)
        print("Sauvegarde terminée en {}".format(msgtps))
    ===> Bonnes sauvegardes!

  2. #2
    Expert confirmé
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 486
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 486
    Billets dans le blog
    6
    Par défaut
    Bonjour,

    Petites améliorations de la fonction précédente "copieincrementale":
    - amélioration des messages du traitement (print) ainsi que des messages d'erreur
    - utilisation de la fonction os.path.relpath en remplacement du calcul par chaînes pour les adresses disque
    - amélioration des commentaires et petites améliorations diverses

    Il suffit de la remplacer dans le script complet (le dernier) du message précédent:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    #############################################################################
    def copieincrementale(rep1, rep2, miroir=False, fnerreur=None, recursion=True, 
                          suiviliens=False):
        """Fait une sauvegarde incrémentale de rep1 sur rep2, c'est à dire ne 
           copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
           - rep1: le répertoire à sauvegarder
           - rep2: le répertoire qui recevra les copies de rep1
           - miroir: si True: supprime les fichiers et répertoires de rep2 absents 
                     de rep1
           - fnerreur: si != None: fonction callback pour les messages d'erreur
           - recursion: si True, traite aussi les sous-répertoires
           - suiviliens: si True, suit les liens symboliques        
           Affiche  en console les mises à jours faites
        """
        # si None => quand la fonction fnerreur est appelée, elle ne fait rien
        fnerreur = (lambda x: None) if fnerreur is None else fnerreur
     
        #=========================================================================
        # normalise et rend absolues les adresses des répertoires donnés
        rep1 = os.path.abspath(os.path.expanduser(rep1))
        rep2 = os.path.abspath(os.path.expanduser(rep2))
     
        #=========================================================================
        # vérifie l'existence et l'accès à rep1
        if not os.path.exists(rep1):
            fnerreur("le répertoire source n'existe pas: {}".format(rep1))
            return # sort de la fonction
        if not os.access(rep1, os.X_OK):
            fnerreur("Pas d'accès au contenu du répertoire source: {}".format(rep1))
            return # sort de la fonction
     
        #=========================================================================
        # vérifie l'existence de rep2 et le crée si nécessaire
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.mkdir(rep2)
                print("Crée le répertoire 2: {}".format(rep2))
            except Exception:
                fnerreur("Impossible de créer le répertoire destination: {}".format(rep2))
                return # sort de la fonction
        else:
            # rep2 existe déjà: on vérifie l'accès
            if not os.access(rep2, os.X_OK):
                fnerreur("Pas d'accès au contenu du répertoire destination: {}".format(rep2))
                return # sort de la fonction
     
        #=========================================================================
        # fait la copie incrémentale 
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur, followlinks=suiviliens):
            for fic in fics:
                nfc1 = os.path.join(repert, fic) # fic avec chemin sur rep1
                nfc1rel = os.path.relpath(nfc1, rep1) # fic avec chemin relatif
                nfc2 = os.path.join(rep2, nfc1rel) # fic avec chemin sur rep2
                if not os.path.exists(nfc2):
                    # crée le chemin nécessaire pour copier le nouveau fichier
                    chemin = os.path.dirname(nfc2)
                    if not os.path.exists(chemin):
                        try:
                            os.makedirs(chemin)
                            print("Crée le nouveau répertoire: {}".format(chemin))
                        except Exception:
                            fnerreur("Impossible de créer le répertoire: {}".format(chemin))
                            continue # => passer au fichier suivant
                    # copie le nouveau fichier
                    try:
                        copy2(nfc1, nfc2)
                        print("Copie le nouveau fichier: {}".format(nfc2))
                    except Exception:
                        fnerreur("Impossible de copier le nouveau fichier: {}".format(nfc2)) 
                        # passe au fichier suivant   
                else:
                    # le fichier fic existe déjà sur rep2: on compare les dates
                    try:
                        temps1, temps2 = os.path.getmtime(nfc1), os.path.getmtime(nfc2)
                    except Exception:
                        fnerreur("Impossible de calculer les dates de mise à jour de {} et/ou de {}".format(nfc1, nfc2))
                        continue # passe au fichier suivant
                    try:
                        if temps1 > temps2:
                            # copie le fichier fic de rep1, plus récent que celui de rep2
                            copy2(nfc1, nfc2) 
                            print("Copie le fichier plus récent: {}".format(nfc2))
                    except Exception:
                        fnerreur("Impossible de copier le fichier plus récent: {}".format(nfc2))               
                        # passe au fichier suivant   
     
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # traite l'effet miroir s'il est demandé
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur, followlinks=suiviliens):
                # supprime les fichiers de rep2 absents de rep1
                for fic in fics:
                    nfc2 = os.path.join(repert, fic) # fic avec chemin sur rep2
                    nfc2rel = os.path.relpath(nfc2, rep2) # fic avec chemin relatif
                    nfc1 = os.path.join(rep1, nfc2rel) # fic avec chemin sur rep1
                    if not os.path.exists(nfc1):
                        # le fichier fic de rep2 absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            print("Miroir => supprime le fichier: {}".format(nfc2))
                        except Exception:
                            fnerreur("Impossible de supprimer le fichier {}".format(nfc2))
     
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                    nrc2 = os.path.join(repert, reps[i]) # rep avec chemin sur rep2
                    nrc2rel = os.path.relpath(nrc2, rep2) # rep avec chemin relatif
                    nrc1 = os.path.join(rep1, nrc2rel) # rep avec chemin sur rep1
                    if not os.path.exists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            rmtree(nrc2)
                            print("Miroir => supprime le répertoire: {}".format(nrc2))
                            reps.pop(i) # on retire rep[i] de l'exploration suivante
                        except Exception:
                            fnerreur("miroir: impossible de supprimer le répertoire {}".format(nrc2))
    ===> Bonnes sauvegardes!

  3. #3
    Nouveau candidat au Club
    Homme Profil pro
    Administrateur systèmes et réseaux
    Inscrit en
    Août 2020
    Messages
    2
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Administrateur systèmes et réseaux

    Informations forums :
    Inscription : Août 2020
    Messages : 2
    Par défaut Petite question
    Merci pour ce magnifique outil!

  4. #4
    Expert confirmé
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 486
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 486
    Billets dans le blog
    6
    Par défaut Synchroniser 2 répertoires
    Bonjour

    Citation Envoyé par yitian Voir le message
    Merci pour ce magnifique outil!
    Merci, ça fait plaisir!


    On peut utiliser cet outil de sauvegarde pour une autre utilisation: synchroniser deux répertoires:

    => J'ai deux répertoires qui devraient être identiques mais qui ne le sont pas (ou je ne suis pas sûr qu'ils le sont). Par exemple parce que l'un est mis à jour avec mon PC de base et l'autre par mon portable en déplacement.

    Pour les "synchroniser", il suffit que chaque répertoire mette à jour l'autre de toutes les nouveautés (fichiers nouveaux et plus récents). Il suffit donc d'exécuter le programme de copie incrémentale dans les deux sens!.
    Il y a cependant plusieurs conditions à respecter:

    - L'option "miroir" ne doit pas être utilisée !.

    - Les horloges des deux PC doivent être réglées sur la même heure pendant les modifications de chaque répertoire (puisque les fichiers gardent l'heure de mise à jour), ce qui est facile à faire si les deux horloges sont réglées automatiquement sur Internet (et sur le même fuseau horaire!).

    - Le même fichier ne doit pas avoir été modifié sur les deux PC, parce que sinon, seule la dernière version serait conservée! On pourrait supprimer cette condition si on conservait l'heure de la dernière synchronisation. Cela permettrait d'identifier la modification ultérieure du même fichier dans les deux répertoires, et d'en conserver les deux versions (en renommant le fichier le moins récent) en affichant une alerte. Il faudrait, bien sûr, modifier le programme en conséquence.

    Comme après cette opération, les deux répertoires sont identiques, on peut, si besoin, supprimer l'un de ces répertoires en étant sûr de ne rien supprimer de nouveau ou plus récent.

    Comme sécurité, on pourrait modifier le programme de copie incrémentale pour ajouter une option "simuler", qui afficherait les copies à faire sans les exécuter réellement.

    Si ça intéresse quelqu'un, je pourrais faire une version spécifique de ce programme de synchronisation...

    => Bonne synchronisation !

  5. #5
    Membre Expert
    Homme Profil pro
    Enseignant
    Inscrit en
    Juin 2013
    Messages
    1 617
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Enseignant
    Secteur : Enseignement

    Informations forums :
    Inscription : Juin 2013
    Messages : 1 617
    Par défaut
    Beau travail encore, bravo !
    J'essaierai un de ces jours.
    J'ai une question et elle me trotte dans la tête depuis longtemps : pourquoi utiliser argparse et non lancer le script python via la fonction et des arguments ?
    Je trouverai cela plus simple que d'aller en console.
    Je suis sous linux mais je ne pense pas que la réponse dépende de l'OS.

  6. #6
    Expert confirmé
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 486
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 486
    Billets dans le blog
    6
    Par défaut
    Bonjour marco056

    Citation Envoyé par marco056 Voir le message
    Beau travail encore, bravo !
    Merci!

    Citation Envoyé par marco056 Voir le message
    J'ai une question et elle me trotte dans la tête depuis longtemps : pourquoi utiliser argparse et non lancer le script python via la fonction et des arguments ?
    Je trouverai cela plus simple que d'aller en console.
    Voilà le principe que j'ai adopté. Avec cx_freeze ou pyinstaller, on peut convertir le programme Python en programme autonome ("standalone") pouvant être exécuté sur un PC sans Python. Et en utilisant un petit code en langage console pour chaque répertoire qu'on veut sauvegarder, on prépare ainsi un lancement simple (double-clic sous Windows) de chacune des opérations de sauvegarde. Et sauvegarder un nouveau répertoire ne concerne qu'un fichier batch de quelques lignes.

    Mais on peut faire autrement avec une solution 100% Python, et même avec un programme graphique! Sans argparse, il faut tout de même pouvoir transmettre les arguments d'une façon ou d'une autre! Et comme dans la plupart des cas on sauvegarde les mêmes répertoires, il faut pouvoir les mémoriser d'une session à l'autre dans un fichier texte ou même un fichier de base de données en sqlite3. Ça ne me parait pas plus simple: à quelle solution pensais-tu ?.

  7. #7
    Membre Expert
    Homme Profil pro
    Enseignant
    Inscrit en
    Juin 2013
    Messages
    1 617
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Enseignant
    Secteur : Enseignement

    Informations forums :
    Inscription : Juin 2013
    Messages : 1 617
    Par défaut
    Si l'essai s'avère concluant (mais un peu trop de boulot en ce moment et trop de soleil, donc grandes tentations ), je verrais bien l'ensemble avec tkinter.
    Je n'ai pas ton expérience et je comprends ton raisonnement. Ceci dit, je n'ai jamais réussi à faire un exécutable facilement sous Ubuntu et j'y suis moins sensible.

Discussions similaires

  1. Réponses: 8
    Dernier message: 20/04/2018, 00h38
  2. Réponses: 1
    Dernier message: 31/08/2010, 19h42
  3. Faire une sauvegarde complete du disque
    Par baert dans le forum Administration système
    Réponses: 3
    Dernier message: 19/04/2007, 19h29
  4. Effectuer une sauvegarde
    Par Ultra-FX dans le forum Administration système
    Réponses: 8
    Dernier message: 19/06/2004, 14h04
  5. batch pour faire une sauvegarde
    Par bibiodp dans le forum Scripts/Batch
    Réponses: 4
    Dernier message: 13/08/2003, 13h09

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo