Package Camelot :: Package camelot :: Package core :: Module backup |
[frames] | no frames]

# Source Code for Module Camelot.camelot.core.backup

      1  import sqlalchemy
      2  from camelot.core.utils import ugettext as _
      3
      4 -class BackupMechanism(object):
      5      """Create a backup of the current database to an sqlite database stored in
      6      filename.
      7
      8      The backupmechanism always considers the schema of the backed up database
      9      as the master, and never that of the backup.  This means that when a backup
     10      is made, the backup file is first removed, and then filled with the tables
     11      from the the database to backup.  When a restore is done, the schema of the
     12      database is not touched, but the tables are emptied and the data from the
     13      backup is copied into the existing schema.
     14      """
     15
     16 -    def __init__(self, filename):
     17          """Backup and restore to a local file using it as an sqlite database
     18          """
     19          self._filename = filename
     20
     21 -    def backup_table_filter(self, from_table):
     22          """
     23          Method used to filter which tables should be backed up, overwrite this method
     24          for taking into account specific schema issues.
     25
     26          :from_table: the table that is considered for backup
     27          :return: True when the table should be backed up
     28          """
     29          return True
     30
     31 -    def restore_table_filter(self, from_table):
     32          """
     33          Method used to filter which tables should be restored, overwrite this method
     34          for taking into account specific schema issues.  restore_table_filter is different
     35          from backup_table_filter, since we might want to store data in the backup that
     36          should not be restored, like schema version information.
     37
     38          :from_table: the table that is considered for backup
     39          :return: True when the table should be restored
     40          """
     41          return True
     42
     43 -    def prepare_schema_for_restore(self, from_engine, to_engine):
     44          """This method will be called before the actual restore starts.  It allows bringing
     45          the schema at the same revision as the backed up data.
     46          """
     47          pass
     48
     49 -    def update_schema_after_restore(self, from_engine, to_engine):
     50          """This method will be called after the restore has been done.  It allows bringing
     51          the schema at the revision the application expects.
     52          """
     53          pass
     54
     55 -    def backup(self):
     56          """Generator function that yields tuples :
     57          (numer_of_steps_completed, total_number_of_steps, description_of_current_step)
     58          while performing a backup.
     59          """
     60          import os
     61          from sqlalchemy import create_engine, MetaData
     62          import settings
     63
     64          yield (0, 0, _('Analyzing database structure'))
     65          from_engine = settings.ENGINE()
     66          from_meta_data = MetaData()
     67          from_meta_data.bind = from_engine
     68          from_meta_data.reflect()
     69
     70          yield (0, 0, _('Preparing backup file'))
     71          if os.path.exists(self._filename):
     72              os.remove(self._filename)
     73          to_engine   = create_engine('sqlite:///%s'%self._filename)
     74          to_meta_data = MetaData()
     75          to_meta_data.bind = to_engine
     76          #
     77          # Only copy tables, to prevent issues with indices and constraints
     78          #
     79          from_and_to_tables = []
     80          for from_table in from_meta_data.sorted_tables:
     81              if self.backup_table_filter(from_table):
     82                  to_table = from_table.tometadata(to_meta_data)
     83                  to_table.create(to_engine)
     84                  from_and_to_tables.append((from_table, to_table))
     85
     86          number_of_tables = len(from_and_to_tables)
     87          for i,(from_table, to_table) in enumerate(from_and_to_tables):
     88              yield (i, number_of_tables, _('Copy data of table %s')%from_table.name)
     89              self.copy_table_data(from_table, to_table)
     90          yield (number_of_tables, number_of_tables, _('Backup completed'))
     91
     92 -    def restore(self):
     93          """Generator function that yields tuples :
     94          (numer_of_steps_completed, total_number_of_steps, description_of_current_step)
     95          while performing a restore.
     96          """
     97          import os
     98          from sqlalchemy import create_engine, MetaData
     99          import settings
    100
    101          yield (0, 0, _('Open backup file'))
    102          if not os.path.exists(self._filename):
    103              raise Exception('Backup file does not exist')
    104          from_engine   = create_engine('sqlite:///%s'%self._filename)
    105
    106          yield (0, 0, _('Prepare database for restore'))
    107          to_engine = settings.ENGINE()
    108          self.prepare_schema_for_restore(from_engine, to_engine)
    109
    110          yield (0, 0, _('Analyzing backup structure'))
    111          from_meta_data = MetaData()
    112          from_meta_data.bind = from_engine
    113          from_meta_data.reflect()
    114
    115          yield (0, 0, _('Analyzing database structure'))
    116          to_meta_data = MetaData()
    117          to_meta_data.bind = to_engine
    118          to_meta_data.reflect()
    119          to_tables = list(table for table in to_meta_data.sorted_tables if self.restore_table_filter(table))
    120          number_of_tables = len(to_tables)
    121
    122          for i,to_table in enumerate(reversed(to_tables)):
    123              yield (i, number_of_tables*2, _('Delete data from table %s')%to_table.name)
    124              self.delete_table_data(to_table)
    125
    126          for i,to_table in enumerate(to_tables):
    127              if to_table.name in from_meta_data.tables:
    128                  yield (number_of_tables+i, number_of_tables*2, _('Copy data from table %s')%to_table.name)
    129                  self.copy_table_data(from_meta_data.tables[to_table.name], to_table)
    130
    131          yield (1, 1, _('Update schema after restore'))
    132          self.update_schema_after_restore(from_engine, to_engine)
    133          yield (1, 1, _('Restore completed'))
    134
    135 -    def delete_table_data(self, to_table):
    136          """This method might be subclassed to turn off/on foreign key checks"""
    137          to_connection = to_table.bind.connect()
    138          to_connection.execute(to_table.delete())
    139          to_connection.close()
    140
    141 -    def copy_table_data(self, from_table, to_table):
    142          from_connection = from_table.bind.connect()
    143          to_connection = to_table.bind.connect()
    144          query = sqlalchemy.select([from_table])
    145          for row in from_connection.execute(query).fetchall():
    146              data = dict((key, getattr(row, key)) for key in row.keys())
    147              to_connection.execute(to_table.insert(values=data))
    148          from_connection.close()
    149          to_connection.close()
    150