1 import sqlalchemy
2 from camelot.core.utils import ugettext as _
3
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
17 """Backup and restore to a local file using it as an sqlite database
18 """
19 self._filename = filename
20
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
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
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
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
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
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
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
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
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