nutools/pyulib/src/uapps/plbck.py

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