450 lines
16 KiB
Python
Executable File
450 lines
16 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
|
|
|
u"""%(scriptname)s: Gérer des fichiers de sauvegarde
|
|
|
|
USAGE
|
|
%(scriptname)s [options] bckdir
|
|
|
|
Les fichiers de sauvegarde sont de la forme [PREFIX]yyyymmdd.n[SUFFIX]
|
|
En fonction des fichiers présents dans bckdir, ce script peut afficher:
|
|
- le nom du fichier pour la prochaine sauvegarde
|
|
- le nom du fichier de la dernière sauvegarde
|
|
- le nom des fichiers de sauvegarde à supprimer selon la politque actuelle
|
|
|
|
Si dans les fichiers du répertoire, il y a plusieurs couples (PREFIX, SUFFIX),
|
|
alors les traitements sont répétés pour chacun des couples existant, qui forment
|
|
chacun un groupe. Si un préfixe et un suffixe sont spécifiés avec les options -P
|
|
et -S, alors seuls les fichiers correspondant à ce groupe sont traités.
|
|
|
|
OPTIONS
|
|
-c Afficher le nom à utiliser pour la prochaine sauvegarde (CURRENT)
|
|
-p Afficher le nom de la dernière sauvegarde effectuée (PREVIOUS), ou une
|
|
ligne vide s'il n'y a pas encore de sauvegardes.
|
|
-D Afficher les noms des sauvegardes à supprimer (DELETES). L'affichage se
|
|
fait un fichier par ligne, et se termine par une ligne vide.
|
|
Par défaut, afficher -c -p -D
|
|
-e Afficher les valeurs CURRENT, PREVIOUS et DELETES comme des variables
|
|
shell au lieu de lignes. DELETES est affiché sous forme de tableau. Par
|
|
exemple:
|
|
index=0
|
|
CURRENT='backup-20120507.0.tar.gz'
|
|
PREVIOUS='backup-20120506.0.tar.gz'
|
|
DELETES=('backup-20120505.0.tar.gz' 'backup-20120504.0.tar.gz' 'backup-20120503.0.tar.gz')
|
|
Chaque groupe se voit attribuer un index commençant à 0.
|
|
--bcmd
|
|
Dans le mode -e, spécifier une commande à insérer avant le premier
|
|
groupe. Quand cette commande est lancée, index==-1
|
|
--cmd
|
|
Dans le mode -e, spécifier une commande à insérer après chaque groupe.
|
|
--ecmd
|
|
Dans le mode -e, spécifier une commande à insérer après le dernier
|
|
groupe.
|
|
-P PREFIX
|
|
-S SUFFIX
|
|
Si aucun fichier n'est trouvé, valeur par défaut du préfixe et du
|
|
suffixe des fichiers de sauvegarde.
|
|
--prefix PREFIX
|
|
--suffix SUFFIX
|
|
Spécifier le préfixe et le suffixe des fichiers de sauvegarde sous forme
|
|
d'expressions régulières. Seuls les fichiers correspondant à ces valeurs
|
|
sont considérés.
|
|
Ces valeurs ne permettent pas de déterminer la valeur du préfixe et du
|
|
suffixe à utiliser si aucun fichier n'existe. Il faut utiliser les
|
|
options -P et -S pour cela.
|
|
Ces options n'ont pas de valeur par défaut, sauf si les options -P et -S
|
|
sont spécifiées.
|
|
-N max_days[=%(DEFAULT_MAX_DAYS)i]
|
|
Nombre maximum de *jours* de sauvegarde gardés, étant entendu que l'on
|
|
peut faire plusieurs sauvegardes par jour. C'est en fonction de cette
|
|
valeur que le calcul des fichiers à supprimer est effectué.
|
|
Une valeur de 0 signifie que l'on ne veut garder aucune sauvegarde.
|
|
-M max_backups[=%(DEFAULT_MAX_BACKUPS)i]
|
|
Nombre total maximum de fichiers de sauvegarde gardés, sans tenir compte
|
|
du jour auquel la sauvegarde a été effectuée. C'est en fonction de cette
|
|
valeur que le calcul des fichiers à supprimer est effectué.
|
|
Cette valeur ne doit pas être inférieure à max_days."""
|
|
|
|
DEFAULT_MAX_DAYS=15
|
|
DEFAULT_MAX_BACKUPS=30
|
|
|
|
import os, sys, re
|
|
from os import path
|
|
|
|
from ulib.all import *
|
|
|
|
class Backup(object):
|
|
"""Ensemble de fichier de sauvegardes qui partagent un même préfixe et un même suffixe
|
|
"""
|
|
bm = None
|
|
bckdir = None
|
|
prefix = None
|
|
suffix = None
|
|
|
|
bydates = None
|
|
valid = None
|
|
current = None
|
|
previous = None
|
|
deletes = None
|
|
|
|
def __init__(self, bm, bckdir, prefix=None, suffix=None):
|
|
self.bm = bm
|
|
self.bckdir = bckdir
|
|
if prefix is None: prefix = ''
|
|
if suffix is None: suffix = ''
|
|
self.prefix = prefix
|
|
self.suffix = suffix
|
|
self.bydates = {}
|
|
self.valid = False
|
|
|
|
RE_BCKFILE = re.compile(r'(.*)(\d{4})(\d{2})(\d{2})\.(\d+)(.*)')
|
|
def add(self, bckname):
|
|
"""Ajouter un fichier de sauvegarde à la liste.
|
|
|
|
bckname est le nom du fichier situé dans self.bckdir
|
|
"""
|
|
mo = self.RE_BCKFILE.match(bckname)
|
|
if mo is None: raise ValueError("%s: n'est pas un fichier de sauvegarde valide" % bckname)
|
|
|
|
prefix, year, month, day, num, suffix = mo.groups()
|
|
if prefix != self.prefix or suffix != self.suffix:
|
|
raise ValueError("(prefix, suffix) ne sont pas cohérents: %r != %r",
|
|
(prefix, suffix),
|
|
(self.prefix, self.suffix),
|
|
)
|
|
|
|
year, month, day, num = map(int, (year, month, day, num))
|
|
date = Date(day, month, year)
|
|
|
|
bckinfo = {'file': path.join(self.bckdir, bckname), 'date': date, 'num': num}
|
|
bynums = self.bydates.get(date, None)
|
|
if bynums is None:
|
|
bynums = {}
|
|
self.bydates[date] = bynums
|
|
bynums[num] = bckinfo
|
|
self.valid = False
|
|
|
|
def __build_bckinfo(self, date=None, num=0):
|
|
"""Contruire un dictionnaire bckinfo à partir de self.prefix,
|
|
self.suffix et la date et le numéro spécifiés.
|
|
"""
|
|
if date is None: date = Date()
|
|
bckname = "%s%04i%02i%02i.%i%s" % (
|
|
self.prefix,
|
|
date.year, date.month, date.day, num,
|
|
self.suffix,
|
|
)
|
|
bckinfo = {'file': path.join(self.bckdir, bckname), 'date': date, 'num': num}
|
|
return bckinfo
|
|
|
|
def __sortfunc(self, bckinfo1, bckinfo2):
|
|
"""Comparer deux dictionnaires bckinfo
|
|
"""
|
|
c = cmp(bckinfo1['date'], bckinfo2['date'])
|
|
if c != 0: return c
|
|
c = cmp(bckinfo1['num'], bckinfo2['num'])
|
|
return c
|
|
|
|
def __sorted(self, bydates):
|
|
"""Créer une liste inversement triée de dictionnaires bckinfo à partir
|
|
de bydates.
|
|
"""
|
|
bckinfos = []
|
|
dates = bydates.keys()
|
|
dates.sort()
|
|
for date in dates:
|
|
nums = bydates[date].keys()
|
|
nums.sort()
|
|
for num in nums:
|
|
bckinfos.append(bydates[date][num])
|
|
bckinfos.reverse()
|
|
return bckinfos
|
|
|
|
def __compute_values(self):
|
|
"""Calculer current, previous et deletes en fonction de la valeur
|
|
actuelle de self.bydates
|
|
"""
|
|
bydates = self.bydates.copy()
|
|
bckinfos = self.__sorted(bydates)
|
|
now = Date()
|
|
|
|
# calculer previnfo et curinfo
|
|
if bckinfos:
|
|
previnfo = bckinfos[0]
|
|
if previnfo['date'] == now:
|
|
curinfo = self.__build_bckinfo(now, previnfo['num'] + 1)
|
|
else:
|
|
curinfo = self.__build_bckinfo(now, 0)
|
|
else:
|
|
previnfo = None
|
|
curinfo = self.__build_bckinfo(now, 0)
|
|
|
|
## calculer delinfos
|
|
max_days, max_backups = self.bm.max_days, self.bm.max_backups
|
|
delinfos = []
|
|
dates = bydates.keys()
|
|
dates.sort()
|
|
# d'abord supprimer tous les fichiers correspondant aux dates périmées
|
|
if len(dates) > max_days:
|
|
if max_days > 0: dates2delete = dates[:-max_days]
|
|
else: dates2delete = dates
|
|
for date in dates2delete:
|
|
for num in bydates[date].keys():
|
|
delinfos.append(bydates[date][num])
|
|
del bydates[date][num]
|
|
# ensuite, supprimer les sauvegardes journalières supplémentaires
|
|
# jusqu'à ce qu'on ne dépasse plus le nombre de fichiers de sauvegarde
|
|
# maximum.
|
|
bckinfos = self.__sorted(bydates)
|
|
while len(bckinfos) > max_backups:
|
|
dates = bydates.keys()
|
|
dates.sort()
|
|
for date in dates:
|
|
nums = bydates[date].keys()
|
|
nums.sort()
|
|
if len(nums) <= 1: continue
|
|
num = nums[0]
|
|
delinfos.append(bydates[date][num])
|
|
del bydates[date][num]
|
|
break
|
|
bckinfos = self.__sorted(bydates)
|
|
delinfos.sort(self.__sortfunc)
|
|
|
|
# fin du traitement
|
|
self.current = curinfo['file']
|
|
if previnfo is None: self.previous = None
|
|
else: self.previous = previnfo['file']
|
|
self.deletes = [delinfo['file'] for delinfo in delinfos]
|
|
self.valid = True
|
|
|
|
def get_current(self):
|
|
if not self.valid: self.__compute_values()
|
|
return self.current
|
|
|
|
def get_previous(self):
|
|
if not self.valid: self.__compute_values()
|
|
return self.previous
|
|
|
|
def get_deletes(self):
|
|
if not self.valid: self.__compute_values()
|
|
return self.deletes
|
|
|
|
class BackupManager(object):
|
|
"""Objet permettant de classer un ensemble de fichiers de sauvegarde qui ont
|
|
le même préfixe et le même suffixe.
|
|
"""
|
|
bckdir = None
|
|
re_prefix = None
|
|
re_suffix = None
|
|
def_prefix = None
|
|
def_suffix = None
|
|
max_days = DEFAULT_MAX_DAYS
|
|
max_backups = DEFAULT_MAX_BACKUPS
|
|
backups = None
|
|
|
|
def __init__(self, bckdir, re_prefix=None, re_suffix=None, def_prefix=None, def_suffix=None):
|
|
self.bckdir = bckdir
|
|
if re_prefix is not None: self.re_prefix = re_prefix
|
|
if re_suffix is not None: self.re_suffix = re_suffix
|
|
if def_prefix is not None: self.def_prefix = def_prefix
|
|
if def_suffix is not None: self.def_suffix = def_suffix
|
|
|
|
def __fix_max_vars(self):
|
|
max_days, max_backups = self.max_days, self.max_backups
|
|
if max_days < 0: max_days = 0
|
|
if max_backups < max_days: max_backups = max_days
|
|
self.max_days, self.max_backups = max_days, max_backups
|
|
|
|
RE_BCKFILE = re.compile(r'(.*)(\d{8}\.\d+)(.*)')
|
|
def __build_backups(self):
|
|
backups = {}
|
|
for bckname in os.listdir(self.bckdir):
|
|
# vérifier si le fichier a la forme d'un fichier de sauvegarde
|
|
mo = self.RE_BCKFILE.match(bckname)
|
|
if mo is None: continue
|
|
|
|
# si re_prefix et re_suffix sont spécifiés, il doivent correspondre
|
|
prefix, tag, suffix = mo.groups()
|
|
if self.re_prefix is not None:
|
|
if self.re_prefix.match(prefix) is None: continue
|
|
if self.re_suffix is not None:
|
|
if self.re_suffix.match(suffix) is None: continue
|
|
|
|
# enregistrer le fichier trouvé
|
|
backup = backups.get((prefix, suffix), None)
|
|
if backup is None:
|
|
backup = Backup(self, self.bckdir, prefix, suffix)
|
|
backups[(prefix, suffix)] = backup
|
|
backup.add(bckname)
|
|
|
|
# si aucun fichier n'a été trouvé,
|
|
if not backups:
|
|
prefix = self.def_prefix
|
|
suffix = self.def_suffix
|
|
backups[(prefix, suffix)] = Backup(self, self.bckdir, prefix, suffix)
|
|
return backups
|
|
|
|
def get_backups(self):
|
|
if self.backups is None:
|
|
self.__fix_max_vars()
|
|
self.backups = self.__build_backups()
|
|
return self.backups.values()
|
|
|
|
class OutputManager(object):
|
|
"""Affichage de current, previous, deletes
|
|
"""
|
|
shell = None
|
|
bcmd = None
|
|
cmd = None
|
|
ecmd = None
|
|
|
|
def __init__(self, shell=False, bcmd=None, cmd=None, ecmd=None):
|
|
self.shell = shell
|
|
if bcmd is not None: self.bcmd = bcmd
|
|
if cmd is not None: self.cmd = cmd
|
|
if ecmd is not None: self.ecmd = ecmd
|
|
|
|
def header(self, index, first=False):
|
|
if self.shell:
|
|
print "index=%i" % index
|
|
if first and self.bcmd is not None:
|
|
print self.bcmd
|
|
|
|
def quote_shell(self, s):
|
|
return "'%s'" % s.replace("'", "'\\''")
|
|
|
|
def print_current(self, current):
|
|
if self.shell:
|
|
print "CURRENT=%s" % (self.quote_shell(current))
|
|
else:
|
|
print current
|
|
|
|
def print_previous(self, previous):
|
|
if previous is None: previous = ""
|
|
if self.shell:
|
|
print "PREVIOUS=%s" % (self.quote_shell(previous))
|
|
else:
|
|
print previous
|
|
|
|
def print_deletes(self, deletes):
|
|
deletes = seqof(deletes)
|
|
if self.shell:
|
|
deletes = [self.quote_shell(s) for s in deletes]
|
|
print "DELETES=(%s)" % " ".join(deletes)
|
|
else:
|
|
for delete in deletes:
|
|
print delete
|
|
print
|
|
|
|
def footer(self, index, last=False):
|
|
if self.shell:
|
|
if not last and self.cmd is not None:
|
|
print self.cmd
|
|
elif last and self.ecmd is not None:
|
|
print self.ecmd
|
|
|
|
def display_help():
|
|
uprint(__doc__ % globals())
|
|
|
|
def run_plbck():
|
|
options, longoptions = build_options([
|
|
(None, 'help', "Afficher l'aide"),
|
|
('c', 'current'),
|
|
('p', 'previous'),
|
|
('D', 'deletes'),
|
|
('e', 'shell'),
|
|
(None, 'bcmd='),
|
|
(None, 'cmd='),
|
|
(None, 'ecmd='),
|
|
('P:', 'def-prefix='),
|
|
('S:', 'def-suffix='),
|
|
(None, ('prefix=', 're-prefix=')),
|
|
(None, ('suffix=', 're-suffix=')),
|
|
('N:', ('days=', 'max-days=')),
|
|
('M:', ('count=', 'max-backups=')),
|
|
])
|
|
options, args = get_args(None, options, longoptions)
|
|
|
|
show_current = False
|
|
show_previous = False
|
|
show_deletes = False
|
|
show_auto = True
|
|
shell_vars = False
|
|
bcmd = None
|
|
cmd = None
|
|
ecmd = None
|
|
def_prefix = None
|
|
def_suffix = None
|
|
re_suffix = None
|
|
re_prefix = None
|
|
max_days = None
|
|
max_backups = None
|
|
for option, value in options:
|
|
if option in ('--help', ):
|
|
display_help()
|
|
sys.exit(0)
|
|
elif option in ('-c', '--current'):
|
|
show_current = True
|
|
show_auto = False
|
|
elif option in ('-p', '--previous'):
|
|
show_previous = True
|
|
show_auto = False
|
|
elif option in ('-D', '--deletes'):
|
|
show_deletes = True
|
|
show_auto = False
|
|
elif option in ('-e', '--shell'):
|
|
shell_vars = True
|
|
elif option in ('--bcmd', ):
|
|
bcmd = value
|
|
elif option in ('--cmd', ):
|
|
cmd = value
|
|
elif option in ('--ecmd', ):
|
|
ecmd = value
|
|
elif option in ('-P', '--def-prefix'):
|
|
def_prefix = value
|
|
if re_prefix is None:
|
|
re_prefix = re.compile('%s$' % re.escape(def_prefix))
|
|
elif option in ('-S', '--def-suffix'):
|
|
def_suffix = value
|
|
if re_suffix is None:
|
|
re_suffix = re.compile('%s$' % re.escape(def_suffix))
|
|
elif option in ('--prefix', '--re-prefix'):
|
|
# la correspondance avec re.match() et doit toujours être complète
|
|
if value[-1:] != '$': value += '$'
|
|
re_prefix = re.compile(value)
|
|
elif option in ('--suffix', '--re-suffix'):
|
|
# la correspondance avec re.match() et doit toujours être complète
|
|
if value[-1:] != '$': value += '$'
|
|
re_suffix = re.compile(value)
|
|
elif option in ('-N', '--days', '--max-days'):
|
|
max_days = int(value)
|
|
elif option in ('-M', '--count', '--max-backups'):
|
|
max_backups = int(value)
|
|
|
|
if show_auto:
|
|
show_current = True
|
|
show_previous = True
|
|
show_deletes = True
|
|
|
|
if args: bckdir = args[0]
|
|
else: bckdir = '.'
|
|
|
|
bm = BackupManager(bckdir, re_prefix, re_suffix, def_prefix, def_suffix)
|
|
if max_days is not None: bm.max_days = max_days
|
|
if max_backups is not None: bm.max_backups = max_backups
|
|
om = OutputManager(shell_vars, bcmd, cmd, ecmd)
|
|
|
|
index = -1
|
|
om.header(index, True)
|
|
for backup in bm.get_backups():
|
|
index += 1
|
|
om.header(index)
|
|
if show_current: om.print_current(backup.get_current())
|
|
if show_previous: om.print_previous(backup.get_previous())
|
|
if show_deletes: om.print_deletes(backup.get_deletes())
|
|
om.footer(index)
|
|
om.footer(index, True)
|
|
|
|
if __name__ == '__main__':
|
|
run_plbck()
|