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() |