#!/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()