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

  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.

  8. #8
    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 Adaptation pour la sauvegarde incrémentale des liens symboliques fichiers et répertoires
    Comme j'utilise maintenant les liens symboliques sous Windows, il faut en tenir compte dans la sauvegarde incrémentale. Voir mon post: https://www.developpez.net/forums/d2...s-sous-windows

    Jusqu'à présent, j'avais juste prévu une option "suiviliens" pour donner une valeur à l'argument "followlink" de os.walk. Mais je m'aperçois que la prise en compte des liens symboliques, tant vers des fichiers que vers des répertoires, nécessite un peu plus de réflexion.

    J'ai commencé à voir ce qui se passe si on veut "suivre les liens". Ainsi, avec followlink=True de os.walk et follow_symlinks=True de copy2:
    - les données supplémentaires ciblées par les liens sont sauvegardées
    - ces données sont recopiées à chaque sauvegarde
    - les liens symboliques eux-mêmes disparaissent de la sauvegarde
    - l'option miroir est incompatible (données supplémentaires => supprimées!)

    Pour une simple sauvegarde, il y a beaucoup trop d'inconvénients. Comme on veut que la sauvegarde soit identique à la source, il faut donc abandonner cette possibilité: ce que j'ai fait ici. Il faut donc que les liens symboliques soient copiés comme des liens symboliques! Mais rien n'empêche des courageux d'aller plus loin!

    Avec followlink=False de os.walk (valeur par défaut):

    - pour les liens symboliques "répertoires", ils sont détectés par os.walk comme des répertoires, mais il faut les copier comme des fichiers avec copy2
    et son option follow_symlinks=False (qui n'est pas par défaut) et les supprimer aussi comme des fichiers avec os.remove.

    - pour les liens symboliques "fichiers", ils sont détectés par os.walk comme des fichiers, et ils faut les copier comme des fichiers avec copy2 et son option follow_symlinks=False (qui n'est pas par défaut) et les supprimer aussi comme des fichiers avec os.remove.

    Pour les opérations de copie de liens symboliques, deux bizarreries:

    - copy2 ne transmet pas la date du lien mais lui affecte la date de l'ordinateur.

    - copy2 refuse de remplacer un lien symbolique existant: il faut donc le supprimer avant...

    La copie des liens symboliques ciblant des répertoires est particulière dans la fonction de sauvegarde, puisqu'ils sont identifiés par os.walk comme des répertoires, mais qu'ils doivent être copiés comme des fichiers avec copy2. Il a donc fallu ajouter ce traitement particulier après le traitement des fichiers, mais avant la partie "miroir", et supprimer de la liste des répertoires les liens ainsi traités pour les boucles suivantes de os.walk. Il faut aussi, bien sûr, prendre en compte les liens symboliques pour le traitement de la partie miroir.

    Il y a aussi une autre modification à faire concernant le calcul des temps des fichiers. Si ce sont des liens symboliques, le temps n'est pas calculé avec la même instruction que pour un fichier normal. En effet, pour un fichier lien, os.path.getmtime(fichierlien) renvoie le temps de la cible.

    Dernier point, puisqu'on peut avoir à copier ou supprimer des liens symboliques, on a absolument besoin des droits administrateurs. Le plus simple est de lancer le programme (avec l'exécutable "python" devant) à partir de la console en mode administrateur. Sous Windows, et pour des raisons pratiques, j'utilise plutôt le code que j'ai décrit dans mon post ici:
    https://www.developpez.net/forums/d2...n-sous-windows. Il existe aussi des modules externes pour faire ça (voir pypi).

    Ce programme a été mis au point sous Windows v11, mais comme je n'ai utilisé que des instructions "multiplatformes", je suppose qu'il fonctionne aussi avec les "Unix", ou avec très peu d'adaptations: à essayer, et me dire ce qu'il en est!

    Voilà le code complet corrigé (fichier que j'ai appelé "Sauve_increm.py"):

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    # -*- coding: utf-8 -*-
     
    """
    Sauvegarde incrémentale
     
    Fait une sauvegarde incrémentale du répertoire rep1 sur rep2, c'est à dire
    ne copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
     
    Option miroir: efface de rep2 les fichiers et sous-répertoires absents
    de rep1
     
    Ce programme est compatible avec la sauvegarde de liens symboliques ciblant
    des fichiers ou des répertoires.
    """
     
    import os
    from shutil import copy2, rmtree
    from datetime import datetime # pour les calculs de temps (date+heure)
    from time import perf_counter # pour le calcul du temps de traitement
     
    #############################################################################
    def secs2tempsiso(secondes):
        """Retourne la date locale sous le format type ISO "aaaa-mm-jj_hh-mm-ss"
           (sans les microsecondes)
        """
        return str(datetime.fromtimestamp(secondes,
                tz=None).replace(microsecond=0).isoformat('_')).replace(':', '-')
     
    #############################################################################
    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):
        """Fait une sauvegarde incrémentale du répertoire 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 sauvegardé 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 chaque message d'erreur
           - recursion: si True, traite aussi les sous-répertoires
        """
        # si None => quand la fonction fnerreur est appelée, elle ne fait rien
        fnerreur = (lambda x: None) if fnerreur is None else fnerreur
     
        #=========================================================================
        # 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):
            fnerreur("Erreur: pas d'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:
                fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie son accès
            if not os.access(rep2, os.X_OK):
                fnerreur("Erreur: pas d'accès à rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # fait la copie incrémentale
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur):
     
            #--------------------------------------------------------------------
            # traitement des fichiers du répertoire repert
            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 os.path.exists(nfc2):
                    # le fichier fic existe déjà sur rep2: copie selon les dates
                    try:
                        if os.path.islink(nfc1):
                            # cas des liens symboliques
                            temps1 = os.lstat(nfc1).st_mtime
                            temps2 = os.lstat(nfc2).st_mtime
                            if temps1 > temps2:
                                os.remove(nfc2) # pour éviter une erreur
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier, plus récent:", nfc1)
                        else:
                            # cas des fichiers normaux
                            temps1 = os.path.getmtime(nfc1)
                            temps2 = os.path.getmtime(nfc2)
                            if temps1 > temps2:
                                copy2(nfc1, nfc2)
                                print("Copie le fichier plus récent:", nfc1)
                    except Exception as msgerr:
                        # erreur possible avec la lecture du temps ou copy2
                        fnerreur(msgerr)
     
                else:
                    # le fichier fic n'existe pas sur rep2: on le copie
                    try:
                        chemin = os.path.dirname(nfc2) # chemin d'accès pour nfc2
                        if not os.path.exists(chemin):
                                os.makedirs(chemin) # création du répertoire
                                print("Crée le nouveau répertoire:", chemin)
                        # copie le nouveau fichier
                        copy2(nfc1, nfc2, follow_symlinks=False)
                        if os.path.islink(nfc1):
                            print("Copie le nouveau lien symbolique fichier", nfc1)
                        else:
                            print("Copie le nouveau fichier:", nfc1)
                    except Exception as msgerr:
                        fnerreur(msgerr)
     
            #--------------------------------------------------------------------
            # les liens symboliques ciblant des répertoires sont détectés par
            # os.walk comme des répertoires mais sont copiés comme des fichiers
            for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                nrc1 = os.path.join(repert, reps[i]) # rep avec chemin sur rep1
                if os.path.islink(nrc1):
                    # nrc1 est un lien symbolique vers un répertoire
                    nrc1rel = os.path.relpath(nrc1, rep1) # rep avec chemin relatif
                    nrc2 = os.path.join(rep2, nrc1rel) # rep avec chemin sur rep2
                    try:
                        if os.path.exists(nrc2):
                            temps1 = os.lstat(nfc1).st_mtime
                            temps2 = os.lstat(nfc2).st_mtime
                            if temps1 > temps2: # nfc1 plus récent
                                os.remove(nrc2) # pour éviter une erreur
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire, plus récent:", nrc1)
                        else: # nrc2 n'existe pas
                            copy2(nrc1, nrc2, follow_symlinks=False)
                            print("Copie le nouveau lien symbolique répertoire:", nrc1)
                    except Exception as msgerr:
                        # erreur possible avec os.remove ou copy2
                        fnerreur(msgerr)
                    reps.pop(i) # on retire reps[i] pour la suite du traitement
     
            #--------------------------------------------------------------------
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # traite l'effet miroir si c'est demandé
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur):
     
                #----------------------------------------------------------------
                # 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)
                            if os.path.islink(nfc2):
                                print("Miroir: supprime le lien symbolique fichier", nfc2)
                            else:
                                print("Miroir: supprime le fichier:", nfc2)
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
                #----------------------------------------------------------------
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers
                    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:
                            if os.path.islink(nrc2):
                                os.remove(nrc2) # supprime le lien comme un fichier
                                print("Miroir: supprime le lien symbolique répertoire:", nrc2)
                            else:
                                rmtree(nrc2) # supprime le répertoire + contenu
                                print("Miroir: supprime le répertoire:", nrc2)
                            reps.pop(i) # retire reps[i] de l'exploration suivante
                        except Exception as msgerr:
                            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")
     
        # 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"]
     
        #========================================================================
        # 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()
     
        #========================================================================
        # Sauvegarde
        erreurs = []
        secs = perf_counter()
        copieincrementale(source, destination, miroir, erreurs.append, recursion)
        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))

    Grâce à "argparse", ce programme est prévu pour être exécuté dans une console avec une ligne de commande, et rien ne vous empêche de l'utiliser comme ça. Mais comme je n'aime pas beaucoup les lignes de commande, je vous donne en bonus un "lanceur" en Python: les données de traitement seront donc écrits dans un éditeur de texte, et c'est l'exécution de ce lanceur qui appellera le programme de sauvegarde dans un processus. Voilà ce lanceur (fichier que j'ai appelé: "Sauve_increm_lanceur.py"):

    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
    # -*- coding: utf-8 -*-
    """Lanceur pour la sauvegarde incrémentale d'un répertoire
       NB: si il y a des liens symboliques: à lancer dans une console avec les
       droits administrateur (sinon: erreur)
    """
    ##############################################################################
    # Données de traitement
     
    #source = r"D:\documents_2"
    #source = r"D:\Images2"
    #source = r"D:\Images2_traitements"
    #source = r"D:\Downloads"
    #source = r"D:\@sauvegardes"
    source = r"D:\pythondev"
     
    #destination = r"S:\documents_2"
    #destination = r"S:\Images2"
    #destination = r"S:\Images2_traitements"
    #destination = r"S:\Téléchargements"
    #destination = r"S:\@sauvegardes"
    destination = r"S:\pythondev"
     
    miroir = True
    recursion = True
     
    ##############################################################################
    # Préparation de l'exécution
     
    import sys
    from subprocess import Popen
     
    programme = "sauve_increm.py" # programme Python à exécuter
    encodage = "utf-8" # autres Windows: "cp1252":Windows, "cp850":console DOS
    dictenv = None # os.environ.copy() # variables d'environnement
     
    # mise en forme des arguments
    arguments =[
    "--source", source,
    "--destination", destination,
    "--miroir", str(miroir),
    "--recursion", str(recursion)
    ]
     
    # construction de la ligne de commande
    commande = ["Python", programme] + arguments
     
    ##############################################################################
    # Exécution
     
    with Popen(commande, stdin=sys.stdin, stdout=sys.stdout,
                   stderr=sys.stdout, universal_newlines=True, bufsize=1,
                   errors='replace', shell=False, env=None,
                   encoding=encodage) as proc:
     
            while proc.poll() is None: # tant que le programme est actif
                pass
     
            rc = proc.poll() # statut d'exécution du programme terminé
     
    ##############################################################################
    # Fin d'exécution
    print()
    print("Fin de la sauvegarde. Statut d'exécution:", rc)
    print()
     
    x = input('Tapez "entrée" pour arrêter') # retarde la fermeture de la console

    A noter qu'avec très peu d'adaptations, ce lanceur peut être utilisé pour lancer n'importe quel exécutable avec transmission des arguments de ligne de commande. J'utilise par exemple ce genre de lanceur pour le traitement vidéo avec l'excellent programme "ffmpeg".

    Bonnes sauvegardes !

  9. #9
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 324
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 324
    Par défaut
    bonjour
    suppose qu'il fonctionne aussi avec les "Unix"
    Peut-être mais, attention, une sauvegarde dite incrémentale sous linux n'a rien à voir ! Incrémentales/incrémentielles est faite sous linux avec une technologie particulière. Pour qu'elle soit incrémentale, on va justement utiliser des liens "dur" et certainement pas des liens symboliques.

    Une incrémentale linux :
    - On peut perdre les fichiers d'origine car
    - première sauvegarde est complète / classique,
    - suivantes utilisent des liens dur (avec première sauvegarde)

    Donc,
    - les suivantes prennent très peu de place (partagent des inodes avec la précédente). Uniquement si sauvegardes sur le même support !
    - On peut effacer la première, la suivante immédiate devient la première et les suivantes restent valides avec tous les contenus. Car justement, nous n'avons PAS de liens symboliques vers une sauvegarde précédente.

    Exemple: Nous sauvegardons 1Go de notre disque,
    - 1ère fait (en plus) 1Go
    - on fait une incrémentale par jour :
    chaque fait 1Go pour notre système de fichier mais en réalité n'utilise que quelques Ko sur le disque

    -------------------------

    Un lien dur ?

    Nous avons de la chance, c'est en fait la même chose que nos références en python !!
    un fichier est une variable, un nom de fichier est un nom de variable et un lien dur est une référence.

    Donc, lorsque l'on supprime la première sauvegarde, en fait, on ne fait que décrémenter la référence sur des fichiers.
    S'il existe d'autres sauvegardes, alors le fichier n'est pas supprimé si présent dans un seul des autres. Si aucune sauvegarde n'utilise cette "référence", alors le fichier est bien supprimé du support.

  10. #10
    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 papajoker

    A ma connaissance, un lien en dur est un lien physique qui couramment ne sera pas distingué des liaisons "normales" du disque par les outils de lecture/écriture. La sauvegarde fonctionnera donc sans problème. A charge, bien sûr, à l'administrateur de ne pas créer des liens en dur n'importe comment, et en conséquence, retrouvera son désordre dans la sauvegarde... Le pire étant des liens en dur qui "bouclent". A noter que Windows supporte aussi les liens en dur (https://learn.microsoft.com/fr-fr/wi...-and-junctions).

    Une question: as-tu essayé sous Linux?

  11. #11
    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 Nouvelle version améliorée
    Voici une nouvelle version améliorée. Rappelons que l'objectif est toujours d'obtenir une sauvegarde identique à la source (avec l'option "miroir"), et que cela nécessite quelques précautions lorsqu'il y a des liens symboliques.

    Amélioration de la fonction de sauvegarde

    Quelques subtilités mieux comprises concernant les liens symboliques sous Windows:

    - os.path.exists(...) renvoie True quand le lien existe, mais pas quand il est cassé (= pointe sur un objet qui n'existe pas). Il faut utiliser os.path.lexists() qui n'a pas ce problème.

    - Quand le lien est cassé, on ne peut plus savoir si c'est un lien vers un fichier ou vers un répertoire.

    - j'ai ajouté une nouvelle option: "forcer" pour forcer la copie intégrale de la source, sans tenir compte des conditions de date. Cette option est surtout importante dans le cas de la reconstruction de la source à partir de la dernière sauvegarde (voir ci-après).

    - A noter que, heureusement, ces liens symboliques sont sauvegardés en conservant l'adresse vers leur cible, ce qui veut dire qu'une copie dans l'autre sens permettra de retrouver la situation d'origine. Mais attention à la copie: elle doit se faire dans les mêmes conditions que pour la sauvegarde, et surtout pas avec la souris dans l'explorateur, ou en copier-coller.

    fonction de sauvegarde "sauve_increm.py":

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    # -*- coding: utf-8 -*-
     
    """
    Sauvegarde incrémentale
     
    Fait une sauvegarde incrémentale du répertoire rep1 sur rep2, c'est à dire
    ne copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
     
    Option miroir: efface de rep2 les fichiers et sous-répertoires absents
    de rep1
     
    Ce programme est compatible avec la sauvegarde de liens symboliques ciblant
    des fichiers ou des répertoires, à condition d'avoir les droits administrateur.
    """
     
    import os
    from shutil import copy2, rmtree
    from datetime import datetime # pour les calculs de temps (date+heure)
    from time import perf_counter # pour le calcul du temps de traitement
     
    #############################################################################
    def secs2tempsiso(secondes):
        """Retourne la date locale sous le format type ISO "aaaa-mm-jj_hh-mm-ss"
           (sans les microsecondes)
        """
        return str(datetime.fromtimestamp(secondes,
                tz=None).replace(microsecond=0).isoformat('_')).replace(':', '-')
     
    #############################################################################
    def cejour():
        """Retourne la date et l'heure de l'ordinateur: "jj/mm/aaaa hh:mm:ss"
        (sans les microsecondes)
        """
        dt = datetime.today()
        return "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}".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,
                                                                    forcer=False):
        """Fait une sauvegarde incrémentale du répertoire 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 sauvegardé 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 les messages d'erreur
           - recursion: si True, sauvegarde aussi les sous-répertoires de rep1
           - forcer: force toutes les copies sans tenir compte des dates
        """
        #=========================================================================
        # si fnerreur==None => quand la fonction est appelée, elle ne fait rien
        fnerreur = (lambda x: None) if fnerreur is None else fnerreur
     
        #=========================================================================
        # 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.path.exists(rep1) and os.access(rep1, os.X_OK)):
            fnerreur("Erreur: rep1 non trouvé ou pas d'accès à son contenu")
            return # impossible de trouver ou de rentrer dans rep1
     
        #=========================================================================
        # vérifie l'existence de rep2 et le crée sinon
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.makedirs(rep2)
                print("Crée le répertoire destination:", rep2)
            except Exception as msgerr:
                fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie son accès
            if not os.access(rep2, os.X_OK):
                fnerreur("Erreur: pas d'accès au contenu de rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # fait la copie incrémentale
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur):
     
            #--------------------------------------------------------------------
            # traitement des fichiers du répertoire repert
            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 os.path.lexists(nfc2):
                    # le fichier fic existe déjà sur rep2
                    try:
                        if os.path.islink(nfc1):
                            # nfc1 est un lien symbolique (même cossé)
                            if os.path.islink(nfc2):
                                # nfc2 est aussi un lien symbolique
                                temps1 = os.lstat(nfc1).st_mtime
                                temps2 = os.lstat(nfc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    os.remove(nfc2) # pour éviter une erreur
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le lien symbolique fichier, plus récent:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                os.remove(nfc2)
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface le répertoire et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                        else:
                            # nfc1 est un fichier normal
                            if os.path.islink(nfc2):
                                # nfc2 est un lien symbolique: on remplace par nfc1
                                os.remove(nfc2) # pour éviter une erreur
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                temps1 = os.path.getmtime(nfc1)
                                temps2 = os.path.getmtime(nfc2)
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le fichier plus récent:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface nfc2 et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                    except Exception as msgerr:
                        # erreur possible avec la lecture du temps ou copy2
                        fnerreur(str(msgerr))
     
                else:
                    # le fichier fic n'existe pas sur rep2: on le recopie
                    try:
                        chemin = os.path.dirname(nfc2) # chemin d'accès pour nfc2
                        if not os.path.exists(chemin):
                                os.makedirs(chemin) # crée le(s) répertoire(s)
                                print("Crée le nouveau répertoire:", chemin)
                        # copie le nouveau fichier (normal ou lien symbolique)
                        copy2(nfc1, nfc2, follow_symlinks=False)
                        if os.path.islink(nfc1):
                            print("Copie le nouveau lien symbolique fichier", nfc1)
                        else:
                            print("Copie le nouveau fichier:", nfc1, " => ", nfc2)
                    except Exception as msgerr:
                        fnerreur(msgerr)
     
            #--------------------------------------------------------------------
            # les liens symboliques ciblant des répertoires sont détectés par
            # os.walk comme des répertoires mais sont copiés comme des fichiers
            for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                nrc1 = os.path.join(repert, reps[i]) # rep avec chemin sur rep1
                if os.path.islink(nrc1):
                    # nrc1 est un lien symbolique vers un répertoire
                    nrc1rel = os.path.relpath(nrc1, rep1) # rep avec chemin relatif
                    nrc2 = os.path.join(rep2, nrc1rel) # rep avec chemin sur rep2
                    try:
                        if os.path.lexists(nrc2):
                            if os.path.islink(nrc2):
                                # nrc2 est un lien symbolique (même cossé)
                                temps1 = os.lstat(nrc1).st_mtime
                                temps2 = os.lstat(nrc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nrc1 plus récent
                                    os.remove(nrc2) # pour éviter une erreur
                                    copy2(nrc1, nrc2, follow_symlinks=False)
                                    print("Copie le lien symbolique répertoire, plus récent:", nrc1)
                            elif os.path.isfile(nrc2):
                                # nrc2 est un fichier normal => copie de nrc1
                                os.remove(nrc2)
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                            else:
                                # nrc2 est un répertoire => copie de nrc1
                                rmtree(nrc2) # efface le répertoire et son contenu
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                        else:
                            # nrc2 n'existe pas
                            copy2(nrc1, nrc2, follow_symlinks=False)
                            print("Copie le nouveau lien symbolique répertoire:", nrc1)
                    except Exception as msgerr:
                        # erreur possible avec os.remove ou copy2
                        fnerreur(msgerr)
                    reps.pop(i) # on retire reps[i] pour la suite du traitement
                # ici: passe au répertoire suivant de la liste (s'il y en a encore)
            # ici: passe à la boucle suivante de os.walk (s'il y en a encore)
     
            #--------------------------------------------------------------------
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # traite l'effet miroir si c'est demandé
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur):
     
                #----------------------------------------------------------------
                # 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.lexists(nfc1):
                        # le fichier fic de rep2, absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            if os.path.islink(nfc2):
                                print("Miroir: supprime le lien symbolique fichier", nfc2)
                            else:
                                print("Miroir: supprime le fichier:", nfc2)
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
                #----------------------------------------------------------------
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers
                    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.lexists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            if os.path.islink(nrc2):
                                os.remove(nrc2) # supprime le lien comme un fichier
                                print("Miroir: supprime le lien symbolique répertoire:", nrc2)
                            else:
                                rmtree(nrc2) # supprime le répertoire + contenu
                                print("Miroir: supprime le répertoire:", nrc2)
                            reps.pop(i) # retire reps[i] de l'exploration suivante
                        except Exception as msgerr:
                            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 à sauvegarder")
        parser.add_argument('-d', '--destination', dest='destination', type=str, required=True, action="store", help="Répertoire destination sauvegardé")
        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, sauvegarde aussi les sous-répertoires")
        parser.add_argument('-f', '--forcer', dest="forcer", type=str2bool, choices=[True, False], default=False, help="Si True, force toutes les copies sans tenir compte des dates")
     
        # 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"]
        forcer = dicoargs["forcer"]
     
        #========================================================================
        # en-tête d'affichage
        print()
        print("="*79)
        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("Forcer les copies: {}".format(forcer))
        print()
     
        #========================================================================
        # Exécution de la sauvegarde
        erreurs = []
        secs = perf_counter()
     
        copieincrementale(source, destination, miroir, erreurs.append, recursion,
                                                                           forcer)
        secs = perf_counter()-secs
     
        #========================================================================
        # Affichage des erreurs s'il y en a
        if erreurs:
            print()
            print("Erreurs: {}".format(len(erreurs)))
            print()
            for erreur in erreurs:
                print(erreur)
            print()
     
        #========================================================================
        print("Sauvegarde terminée en {}".format(secs2jhms(secs)[4]))
        print()

    Amélioration du lanceur

    Comme la fonction de sauvegarde elle-même est codée avec "argparse", elle peut être exécutée dans une console avec les arguments en ligne de commande. On peut aussi faire un lanceur en langage de script shell (.bat sous Windows). Mais je préfère pour ma part un "lanceur" Python pour profiter des facilités d'édition avec sa syntaxe.

    Ici, le lanceur Python a été amélioré et complété pour pouvoir enchaîner plusieurs sauvegardes de répertoires, ce qui est beaucoup plus pratique! Cela permettra de lancer d'un seul coup la sauvegarde de tous les répertoires concernés: Documents, Musiques, Vidéos, etc, sans oublier le répertoire du développement Python!

    Lanceur Python "sauve_increm_lanceur.py"

    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
    # -*- coding: utf-8 -*-
    """Lance la sauvegarde incrémentale d'un répertoire rep1 sur rep2, c'est à dire
     ne copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
    Utilise le fichier "sauve_increm.py" pour l'exécution des sauvegardes'
     
    Option miroir: efface de rep2 les fichiers et sous-répertoires absents de rep1.
     
    S'il y a des liens symboliques à modifier: à lancer dans une console avec les
    droits administrateur (sinon: erreur)
    """
    ##############################################################################
    # Importations
     
    import sys
    from subprocess import Popen
     
    ##############################################################################
    # Données de traitement
     
    # liste des répertoires à sauvegarder.
    # Modèle de ligne: ["rep1", "rep2", miroir, recursion],
    repertoires = [
    [r"C:\chemin\vers\Documents", r"S:\chemin\vers\Documents", True, True],
    [r"C:\chemin\vers\Images", r"S:\chemin\vers\Images", True, True],
    [r"C:\chemin\vers\Musiques", r"S:\chemin\vers\Musiques", True, True],
    [r"C:\chemin\vers\DevPython", r"S:\chemin\vers\DevPython", True, True],
    [r"C:\chemin\vers\Videos", r"S:\chemin\vers\Videos", True, True],
    ]
     
    forcer = False #si True => force toutes les copies sans tenir compte des dates
     
    #############################################################################
    # Vérification des données et préparation de l'exécution
     
    if not repertoires:
     
        print("Erreur: aucune sauvegarde n'est demandée")
        _ = input("Faites 'entrée' pour terminer")
        sys.exit()
     
    for repertoire in repertoires:
     
        if len(repertoire) != 4:
            print("Erreur sur nombre de données du répertoire:", repertoire)
            _ = input("Faites 'entrée' pour terminer")
            sys.exit()
     
        rep1, rep2, miroir, recursion = repertoire
        if not (isinstance(rep1, str) and isinstance(rep2, str) and \
                isinstance(miroir, bool) and isinstance(recursion, bool)):
            print("Erreur de type de données du repertoire:", repertoire)
            _ = input("Faites 'entrée' pour terminer")
            sys.exit()
     
    programme = "sauve_increm.py" # programme Python de sauvegarde à exécuter
    encodage = "utf-8"
     
    ##############################################################################
    # Sauvegarde de tous les répertoires, un par un
     
    for rep1, rep2, miroir, recursion in repertoires:
     
        # construction de la ligne de commande
        rep1, rep2 = '"'+rep1+'"', '"'+rep2+'"'# au cas où il y aurait des espaces
        arguments =[
        "--source", rep1, # répertoire à sauvegarder
        "--destination", rep2, # répertoire de la sauvegarde
        "--miroir", str(miroir), # option miroir
        "--recursion", str(recursion), # option récursion
        "--forcer", str(forcer) # option forcer toutes les copies sans les dates
        ]
        commande = ["Python", programme] + arguments # liste de la commande (list)
        commande = " ".join(commande) # ligne de commande (str)
     
        # exécution
        with Popen(commande, stdin=sys.stdin, stdout=sys.stdout,
                        stderr=sys.stdout, universal_newlines=True, bufsize=1,
                        errors='replace', shell=False, env=None,
                        encoding=encodage) as proc:
     
                while proc.poll() is None: # tant que le programme est actif
                    pass
     
                rc = proc.poll() # statut d'exécution du programme terminé
     
        print()
        print("Fin de la sauvegarde. Statut d'exécution:", rc)
        print()
     
    ##############################################################################
    # Fin d'exécution
    print()
    print("Fin des sauvegardes")
    print()
     
    _ = input('Tapez "entrée" pour arrêter') # retarde la fermeture de la console
    Récupération des données sauvegardées

    Cela n'a pas encore été abordé, mais c'est important. On fait une sauvegarde pour conserver ses données quand la source a perdu son intégrité ou a disparu. Cela peut se produire pour de nombreuses raisons: mauvaise manipulation, panne technique, vol, virus, ransonware, etc, et même incendie ou dégât des eaux...

    Quand ça arrive, on veut, bien sûr, récupérer les données sauvegardées pour reconstituer la source: comment on fait?

    S'il n'y a pas de lien symbolique, pas de problème: il suffit d'une copie simple, voire d'un simple copier-coller. Mais s'il y a des liens symboliques, ça ne marche plus car, dans ce cas, on a vu que les liens symboliques copiés seront remplacés par leur cible: on n'aura donc plus une source identique à la sauvegarde.

    Il est plus simple et plus fiable d'utiliser le présent programme de sauvegarde avec des données inversées: la sauvegarde actuelle devient la nouvelle source, et l'ancienne source devient la nouvelle destination. Bien entendu, dans ce cas, il faut utiliser l'option miroir. Mais il faut aussi neutraliser les conditions de dates, et forcer la copie complète de tous les fichiers (dont les liens symboliques).

    Pour cela, j'ai ajouté une option "forcer" qui est à "False" par défaut, et qu'on met à "True" quand on veut que toutes les copies soient faites, sans tenir compte des conditions sur les dates.

    A noter que même si on supprime l'ancienne source abimées pour forcer la recopie intégrale, on ne peut toujours pas utiliser le copier-coller de l'OS si on a des liens symboliques, puisque ceux-ci seraient remplacés par leurs cibles (au moins sous Windows).


    Bonnes sauvegardes !

  12. #12
    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
    Amélioration de la fonction de sauvegarde

    A l'usage, puisque j'utilise moi-même ce programme de sauvegarde, j'ai déjà rencontré une difficulté particulière: lorsque le disque de sauvegarde est presque plein, le fait de copier tous les fichiers nouveaux et plus récent avant d'activer la fonction miroir (si elle est demandée) peut bloquer la sauvegarde pour disque plein!

    La solution est facile à trouver: il suffit d'inverser les opérations, c'est à dire de commencer le traitement de l'option miroir (si elle est demandée) et de faire les copies après.

    Je profite de cette amélioration pour supprimer une anomalie: si l'option miroir est demandée et que la recursion est désactivée, il faut arrêter le os.walk de l'option miroir à sa première boucle.

    Voilà le programme du message précédent ainsi modifié:

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    # -*- coding: utf-8 -*-
     
    """
    Sauvegarde incrémentale
     
    Fait une sauvegarde incrémentale du répertoire rep1 sur rep2, c'est à dire
    ne copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
     
    Option miroir: efface de rep2 les fichiers et sous-répertoires absents
    de rep1
     
    Ce programme est compatible avec la sauvegarde de liens symboliques ciblant
    des fichiers ou des répertoires, à condition d'avoir les droits administrateur.
    """
     
    import os
    from shutil import copy2, rmtree
    from datetime import datetime # pour les calculs de temps (date+heure)
    from time import perf_counter # pour le calcul du temps de traitement
     
    #############################################################################
    def secs2tempsiso(secondes):
        """Retourne la date locale sous le format type ISO "aaaa-mm-jj_hh-mm-ss"
           (sans les microsecondes)
        """
        return str(datetime.fromtimestamp(secondes,
                tz=None).replace(microsecond=0).isoformat('_')).replace(':', '-')
     
    #############################################################################
    def cejour():
        """Retourne la date et l'heure de l'ordinateur: "jj/mm/aaaa hh:mm:ss"
        (sans les microsecondes)
        """
        dt = datetime.today()
        return "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}".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,
                                                                    forcer=False):
        """Fait une sauvegarde incrémentale du répertoire 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 sauvegardé 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 les messages d'erreur
           - recursion: si True, sauvegarde aussi les sous-répertoires de rep1
           - forcer: force toutes les copies sans tenir compte des dates
        """
        #=========================================================================
        # si fnerreur==None => quand la fonction est appelée, elle ne fait rien
        fnerreur = (lambda x: None) if fnerreur is None else fnerreur
     
        #=========================================================================
        # 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.path.exists(rep1) and os.access(rep1, os.X_OK)):
            fnerreur("Erreur: rep1 non trouvé ou pas d'accès à son contenu")
            return # impossible de trouver ou de rentrer dans rep1
     
        #=========================================================================
        # vérifie l'existence de rep2 et le crée sinon
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.makedirs(rep2)
                print("Crée le répertoire destination:", rep2)
            except Exception as msgerr:
                fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie son accès
            if not os.access(rep2, os.X_OK):
                fnerreur("Erreur: pas d'accès au contenu de rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # traite l'effet miroir si c'est demandé
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur):
     
                #----------------------------------------------------------------
                # 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.lexists(nfc1):
                        # le fichier fic de rep2, absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            if os.path.islink(nfc2):
                                print("Miroir: supprime le lien symbolique fichier", nfc2)
                            else:
                                print("Miroir: supprime le fichier:", nfc2)
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
                #----------------------------------------------------------------
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers
                    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.lexists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            if os.path.islink(nrc2):
                                os.remove(nrc2) # supprime le lien comme un fichier
                                print("Miroir: supprime le lien symbolique répertoire:", nrc2)
                            else:
                                rmtree(nrc2) # supprime le répertoire + contenu
                                print("Miroir: supprime le répertoire:", nrc2)
                            reps.pop(i) # retire reps[i] de l'exploration suivante
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
            #--------------------------------------------------------------------
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # fait la copie incrémentale
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur):
     
            #--------------------------------------------------------------------
            # traitement des fichiers du répertoire repert
            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 os.path.lexists(nfc2):
                    # le fichier fic existe déjà sur rep2
                    try:
                        if os.path.islink(nfc1):
                            # nfc1 est un lien symbolique (même cossé)
                            if os.path.islink(nfc2):
                                # nfc2 est aussi un lien symbolique
                                temps1 = os.lstat(nfc1).st_mtime
                                temps2 = os.lstat(nfc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    os.remove(nfc2) # pour éviter une erreur
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le lien symbolique fichier, plus récent:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                os.remove(nfc2)
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface le répertoire et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                        else:
                            # nfc1 est un fichier normal
                            if os.path.islink(nfc2):
                                # nfc2 est un lien symbolique: on remplace par nfc1
                                os.remove(nfc2) # pour éviter une erreur
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                temps1 = os.path.getmtime(nfc1)
                                temps2 = os.path.getmtime(nfc2)
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le fichier plus récent:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface nfc2 et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                    except Exception as msgerr:
                        # erreur possible avec la lecture du temps ou copy2
                        fnerreur(str(msgerr))
     
                else:
                    # le fichier fic n'existe pas sur rep2: on le recopie
                    try:
                        chemin = os.path.dirname(nfc2) # chemin d'accès pour nfc2
                        if not os.path.exists(chemin):
                                os.makedirs(chemin) # crée le(s) répertoire(s)
                                print("Crée le nouveau répertoire:", chemin)
                        # copie le nouveau fichier (normal ou lien symbolique)
                        copy2(nfc1, nfc2, follow_symlinks=False)
                        if os.path.islink(nfc1):
                            print("Copie le nouveau lien symbolique fichier", nfc1)
                        else:
                            print("Copie le nouveau fichier:", nfc1, " => ", nfc2)
                    except Exception as msgerr:
                        fnerreur(msgerr)
     
            #--------------------------------------------------------------------
            # les liens symboliques ciblant des répertoires sont détectés par
            # os.walk comme des répertoires mais sont copiés comme des fichiers
            for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                nrc1 = os.path.join(repert, reps[i]) # rep avec chemin sur rep1
                if os.path.islink(nrc1):
                    # nrc1 est un lien symbolique vers un répertoire
                    nrc1rel = os.path.relpath(nrc1, rep1) # rep avec chemin relatif
                    nrc2 = os.path.join(rep2, nrc1rel) # rep avec chemin sur rep2
                    try:
                        if os.path.lexists(nrc2):
                            if os.path.islink(nrc2):
                                # nrc2 est un lien symbolique (même cossé)
                                temps1 = os.lstat(nrc1).st_mtime
                                temps2 = os.lstat(nrc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nrc1 plus récent
                                    os.remove(nrc2) # pour éviter une erreur
                                    copy2(nrc1, nrc2, follow_symlinks=False)
                                    print("Copie le lien symbolique répertoire, plus récent:", nrc1)
                            elif os.path.isfile(nrc2):
                                # nrc2 est un fichier normal => copie de nrc1
                                os.remove(nrc2)
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                            else:
                                # nrc2 est un répertoire => copie de nrc1
                                rmtree(nrc2) # efface le répertoire et son contenu
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                        else:
                            # nrc2 n'existe pas
                            copy2(nrc1, nrc2, follow_symlinks=False)
                            print("Copie le nouveau lien symbolique répertoire:", nrc1)
                    except Exception as msgerr:
                        # erreur possible avec os.remove ou copy2
                        fnerreur(msgerr)
                    reps.pop(i) # on retire reps[i] pour la suite du traitement
                # ici: passe au répertoire suivant de la liste (s'il y en a encore)
            # ici: passe à la boucle suivante de os.walk (s'il y en a encore)
     
            #--------------------------------------------------------------------
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
     
    #############################################################################
    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 à sauvegarder")
        parser.add_argument('-d', '--destination', dest='destination', type=str, required=True, action="store", help="Répertoire destination sauvegardé")
        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, sauvegarde aussi les sous-répertoires")
        parser.add_argument('-f', '--forcer', dest="forcer", type=str2bool, choices=[True, False], default=False, help="Si True, force toutes les copies sans tenir compte des dates")
     
        # 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"]
        forcer = dicoargs["forcer"]
     
        #========================================================================
        # en-tête d'affichage
        print()
        print("="*79)
        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("Forcer les copies: {}".format(forcer))
        print()
     
        #========================================================================
        # Exécution de la sauvegarde
        erreurs = []
        secs = perf_counter()
     
        copieincrementale(source, destination, miroir, erreurs.append, recursion,
                                                                           forcer)
        secs = perf_counter()-secs
     
        #========================================================================
        # Affichage des erreurs s'il y en a
        if erreurs:
            print()
            print("Erreurs: {}".format(len(erreurs)))
            print()
            for erreur in erreurs:
                print(erreur)
            print()
     
        #========================================================================
        print("Sauvegarde terminée en {}".format(secs2jhms(secs)[4]))
        print()
    Bonnes sauvegardes !

  13. #13
    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

    Bizarrement, je n'ai pas réussi à modifier le message précédent, peut-être parce qu'il est trop ancien. Alors, j'ajoute ici deux points:

    1 - L'option miroir est une option très intéressante puisqu'elle permet d'obtenir une sauvegarde identique à la source. Mais elle comporte aussi un danger: si vous vous trompez de répertoire source et qu'il est vide avec cette option activée, le programme va supprimer la sauvegarde existante! Il ne s'agit pas d'une erreur: le programme fera ce que vous lui demandez, puisqu'il supprimera de la sauvegarde tout ce qui ne se trouve pas dans la source qui est vide. Il ne vous restera plus qu'à la reconstituer totalement (à condition de s'en apercevoir). Donc: avec l'option miroir activée, ne vous trompez pas de répertoire source!!!

    2 - Correction d'une petite erreur qui m'avait échappée: augmentation de l'indentation des lignes 146 et 147:

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    # -*- coding: utf-8 -*-
     
    """
    Sauvegarde incrémentale
     
    Fait une sauvegarde incrémentale du répertoire rep1 sur rep2, c'est à dire
    ne copie que les fichiers de rep1, nouveaux pour rep2 ou plus récents.
     
    Option miroir: efface de rep2 les fichiers et sous-répertoires absents
    de rep1
     
    Ce programme est compatible avec la sauvegarde de liens symboliques ciblant
    des fichiers ou des répertoires, à condition d'avoir les droits administrateur.
    """
     
    import os
    from shutil import copy2, rmtree
    from datetime import datetime # pour les calculs de temps (date+heure)
    from time import perf_counter # pour le calcul du temps de traitement
     
    #############################################################################
    def secs2tempsiso(secondes):
        """Retourne la date locale sous le format type ISO "aaaa-mm-jj_hh-mm-ss"
           (sans les microsecondes)
        """
        return str(datetime.fromtimestamp(secondes,
                tz=None).replace(microsecond=0).isoformat('_')).replace(':', '-')
     
    #############################################################################
    def cejour():
        """Retourne la date et l'heure de l'ordinateur: "jj/mm/aaaa hh:mm:ss"
        (sans les microsecondes)
        """
        dt = datetime.today()
        return "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}".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,
                                                                    forcer=False):
        """Fait une sauvegarde incrémentale du répertoire 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 sauvegardé 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 les messages d'erreur
           - recursion: si True, sauvegarde aussi les sous-répertoires de rep1
           - forcer: force toutes les copies sans tenir compte des dates
        """
        #=========================================================================
        # si fnerreur==None => quand la fonction est appelée, elle ne fait rien
        fnerreur = (lambda x: None) if fnerreur is None else fnerreur
     
        #=========================================================================
        # 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.path.exists(rep1) and os.access(rep1, os.X_OK)):
            fnerreur("Erreur: rep1 non trouvé ou pas d'accès à son contenu")
            return # impossible de trouver ou de rentrer dans rep1
     
        #=========================================================================
        # vérifie l'existence de rep2 et le crée sinon
        if not os.path.exists(rep2):
            # rep2 n'existe pas: on le crée
            try:
                os.makedirs(rep2)
                print("Crée le répertoire destination:", rep2)
            except Exception as msgerr:
                fnerreur(msgerr)
                return # impossible de créer le répertoire destination rep2
        else:
            # rep2 existe déjà: on vérifie son accès
            if not os.access(rep2, os.X_OK):
                fnerreur("Erreur: pas d'accès au contenu de rep2")
                return # impossible de rentrer dans rep2
     
        #=========================================================================
        # traite l'effet miroir si c'est demandé
        if miroir:
            # lecture de rep2
            for repert, reps, fics in os.walk(rep2, onerror=fnerreur):
     
                #----------------------------------------------------------------
                # 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.lexists(nfc1):
                        # le fichier fic de rep2, absent de rep1 => on le supprime
                        try:
                            os.remove(nfc2)
                            if os.path.islink(nfc2):
                                print("Miroir: supprime le lien symbolique fichier", nfc2)
                            else:
                                print("Miroir: supprime le fichier:", nfc2)
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
                #----------------------------------------------------------------
                # supprime les répertoires de rep2 absents de rep1
                for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers
                    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.lexists(nrc1):
                        # répertoire rep de rep2 absent de rep1 => on le supprime
                        try:
                            if os.path.islink(nrc2):
                                os.remove(nrc2) # supprime le lien comme un fichier
                                print("Miroir: supprime le lien symbolique répertoire:", nrc2)
                            else:
                                rmtree(nrc2) # supprime le répertoire + contenu
                                print("Miroir: supprime le répertoire:", nrc2)
                            reps.pop(i) # retire reps[i] de l'exploration suivante
                        except Exception as msgerr:
                            fnerreur(msgerr)
     
                #--------------------------------------------------------------------
                if not recursion:
                    break # interrompt la boucle os.walk pour empêcher la récursion
     
        #=========================================================================
        # fait la copie incrémentale
        for repert, reps, fics in os.walk(rep1, onerror=fnerreur):
     
            #--------------------------------------------------------------------
            # traitement des fichiers du répertoire repert
            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 os.path.lexists(nfc2):
                    # le fichier fic existe déjà sur rep2
                    try:
                        if os.path.islink(nfc1):
                            # nfc1 est un lien symbolique (même cossé)
                            if os.path.islink(nfc2):
                                # nfc2 est aussi un lien symbolique
                                temps1 = os.lstat(nfc1).st_mtime
                                temps2 = os.lstat(nfc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    os.remove(nfc2) # pour éviter une erreur
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le lien symbolique fichier, plus récent:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                os.remove(nfc2)
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface le répertoire et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le lien symbolique fichier:", nfc1)
                        else:
                            # nfc1 est un fichier normal
                            if os.path.islink(nfc2):
                                # nfc2 est un lien symbolique: on remplace par nfc1
                                os.remove(nfc2) # pour éviter une erreur
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                            elif os.path.isfile(nfc2):
                                # nfc2 est un fichier normal
                                temps1 = os.path.getmtime(nfc1)
                                temps2 = os.path.getmtime(nfc2)
                                if temps1 > temps2 or forcer:
                                    # nfc1 + récent: on le recopie
                                    copy2(nfc1, nfc2, follow_symlinks=False)
                                    print("Copie le fichier plus récent:", nfc1)
                            else:
                                # nfc2 est un répertoire
                                rmtree(nfc2) # efface nfc2 et son contenu
                                copy2(nfc1, nfc2, follow_symlinks=False)
                                print("Copie le fichier:", nfc1)
                    except Exception as msgerr:
                        # erreur possible avec la lecture du temps ou copy2
                        fnerreur(str(msgerr))
     
                else:
                    # le fichier fic n'existe pas sur rep2: on le recopie
                    try:
                        chemin = os.path.dirname(nfc2) # chemin d'accès pour nfc2
                        if not os.path.exists(chemin):
                                os.makedirs(chemin) # crée le(s) répertoire(s)
                                print("Crée le nouveau répertoire:", chemin)
                        # copie le nouveau fichier (normal ou lien symbolique)
                        copy2(nfc1, nfc2, follow_symlinks=False)
                        if os.path.islink(nfc1):
                            print("Copie le nouveau lien symbolique fichier", nfc1)
                        else:
                            print("Copie le nouveau fichier:", nfc1, " => ", nfc2)
                    except Exception as msgerr:
                        fnerreur(msgerr)
     
            #--------------------------------------------------------------------
            # les liens symboliques ciblant des répertoires sont détectés par
            # os.walk comme des répertoires mais sont copiés comme des fichiers
            for i in range(len(reps[:])-1, -1, -1): # parcours à l'envers de reps
                nrc1 = os.path.join(repert, reps[i]) # rep avec chemin sur rep1
                if os.path.islink(nrc1):
                    # nrc1 est un lien symbolique vers un répertoire
                    nrc1rel = os.path.relpath(nrc1, rep1) # rep avec chemin relatif
                    nrc2 = os.path.join(rep2, nrc1rel) # rep avec chemin sur rep2
                    try:
                        if os.path.lexists(nrc2):
                            if os.path.islink(nrc2):
                                # nrc2 est un lien symbolique (même cossé)
                                temps1 = os.lstat(nrc1).st_mtime
                                temps2 = os.lstat(nrc2).st_mtime
                                if temps1 > temps2 or forcer:
                                    # nrc1 plus récent
                                    os.remove(nrc2) # pour éviter une erreur
                                    copy2(nrc1, nrc2, follow_symlinks=False)
                                    print("Copie le lien symbolique répertoire, plus récent:", nrc1)
                            elif os.path.isfile(nrc2):
                                # nrc2 est un fichier normal => copie de nrc1
                                os.remove(nrc2)
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                            else:
                                # nrc2 est un répertoire => copie de nrc1
                                rmtree(nrc2) # efface le répertoire et son contenu
                                copy2(nrc1, nrc2, follow_symlinks=False)
                                print("Copie le lien symbolique répertoire:", nrc1)
                        else:
                            # nrc2 n'existe pas
                            copy2(nrc1, nrc2, follow_symlinks=False)
                            print("Copie le nouveau lien symbolique répertoire:", nrc1)
                    except Exception as msgerr:
                        # erreur possible avec os.remove ou copy2
                        fnerreur(msgerr)
                    reps.pop(i) # on retire reps[i] pour la suite du traitement
                # ici: passe au répertoire suivant de la liste (s'il y en a encore)
            # ici: passe à la boucle suivante de os.walk (s'il y en a encore)
     
            #--------------------------------------------------------------------
            if not recursion:
                break # interrompt la boucle os.walk pour empêcher la récursion
     
     
    #############################################################################
    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 à sauvegarder")
        parser.add_argument('-d', '--destination', dest='destination', type=str, required=True, action="store", help="Répertoire destination sauvegardé")
        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, sauvegarde aussi les sous-répertoires")
        parser.add_argument('-f', '--forcer', dest="forcer", type=str2bool, choices=[True, False], default=False, help="Si True, force toutes les copies sans tenir compte des dates")
     
        # 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"]
        forcer = dicoargs["forcer"]
     
        #========================================================================
        # en-tête d'affichage
        print()
        print("="*79)
        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("Forcer les copies: {}".format(forcer))
        print()
     
        #========================================================================
        # Exécution de la sauvegarde
        erreurs = []
        secs = perf_counter()
     
        copieincrementale(source, destination, miroir, erreurs.append, recursion,
                                                                           forcer)
        secs = perf_counter()-secs
     
        #========================================================================
        # Affichage des erreurs s'il y en a
        if erreurs:
            print()
            print("Erreurs: {}".format(len(erreurs)))
            print()
            for erreur in erreurs:
                print(erreur)
            print()
     
        #========================================================================
        print("Sauvegarde terminée en {}".format(secs2jhms(secs)[4]))
        print()

    Bonnes sauvegardes!

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