1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 from mediagoblin
.db
.base
import Session
18 from mediagoblin
.db
.models
import MediaEntry
, Tag
, MediaTag
, Collection
20 from mediagoblin
.tools
.common
import simple_printer
23 class MigrationManager(object):
25 Migration handling tool.
27 Takes information about a database, lets you update the database
28 to the latest migrations, etc.
31 def __init__(self
, name
, models
, migration_registry
, session
,
32 printer
=simple_printer
):
35 - name: identifier of this section of the database
36 - session: session we're going to migrate
37 - migration_registry: where we should find all migrations to
40 self
.name
= unicode(name
)
42 self
.session
= session
43 self
.migration_registry
= migration_registry
44 self
._sorted
_migrations
= None
45 self
.printer
= printer
48 from mediagoblin
.db
.models
import MigrationData
50 self
.migration_model
= MigrationData
51 self
.migration_table
= MigrationData
.__table
__
54 def sorted_migrations(self
):
56 Sort migrations if necessary and store in self._sorted_migrations
58 if not self
._sorted
_migrations
:
59 self
._sorted
_migrations
= sorted(
60 self
.migration_registry
.items(),
61 # sort on the key... the migration number
62 key
=lambda migration_tuple
: migration_tuple
[0])
64 return self
._sorted
_migrations
67 def migration_data(self
):
69 Get the migration row associated with this object, if any.
71 return self
.session
.query(
72 self
.migration_model
).filter_by(name
=self
.name
).first()
75 def latest_migration(self
):
77 Return a migration number for the latest migration, or 0 if
78 there are no migrations.
80 if self
.sorted_migrations
:
81 return self
.sorted_migrations
[-1][0]
83 # If no migrations have been set, we start at 0.
87 def database_current_migration(self
):
89 Return the current migration in the database.
91 # If the table doesn't even exist, return None.
92 if not self
.migration_table
.exists(self
.session
.bind
):
95 # Also return None if self.migration_data is None.
96 if self
.migration_data
is None:
99 return self
.migration_data
.version
101 def set_current_migration(self
, migration_number
=None):
103 Set the migration in the database to migration_number
104 (or, the latest available)
106 self
.migration_data
.version
= migration_number
or self
.latest_migration
107 self
.session
.commit()
109 def migrations_to_run(self
):
111 Get a list of migrations to run still, if any.
113 Note that this will fail if there's no migration record for
116 assert self
.database_current_migration
is not None
118 db_current_migration
= self
.database_current_migration
121 (migration_number
, migration_func
)
122 for migration_number
, migration_func
in self
.sorted_migrations
123 if migration_number
> db_current_migration
]
126 def init_tables(self
):
128 Create all tables relative to this package
130 # sanity check before we proceed, none of these should be created
131 for model
in self
.models
:
132 # Maybe in the future just print out a "Yikes!" or something?
133 assert not model
.__table
__.exists(self
.session
.bind
)
135 self
.migration_model
.metadata
.create_all(
137 tables
=[model
.__table
__ for model
in self
.models
])
139 def create_new_migration_record(self
):
141 Create a new migration record for this migration set
143 migration_record
= self
.migration_model(
145 version
=self
.latest_migration
)
146 self
.session
.add(migration_record
)
147 self
.session
.commit()
151 Print out a dry run of what we would have upgraded.
153 if self
.database_current_migration
is None:
155 u
'~> Woulda initialized: %s\n' % self
.name_for_printing())
158 migrations_to_run
= self
.migrations_to_run()
159 if migrations_to_run
:
161 u
'~> Woulda updated %s:\n' % self
.name_for_printing())
163 for migration_number
, migration_func
in migrations_to_run():
165 u
' + Would update %s, "%s"\n' % (
166 migration_number
, migration_func
.func_name
))
170 def name_for_printing(self
):
171 if self
.name
== u
'__main__':
172 return u
"main mediagoblin tables"
174 # TODO: Use the friendlier media manager "human readable" name
175 return u
'media type "%s"' % self
.name
177 def init_or_migrate(self
):
179 Initialize the database or migrate if appropriate.
181 Returns information about whether or not we initialized
182 ('inited'), migrated ('migrated'), or did nothing (None)
184 assure_migrations_table_setup(self
.session
)
186 # Find out what migration number, if any, this database data is at,
187 # and what the latest is.
188 migration_number
= self
.database_current_migration
190 # Is this our first time? Is there even a table entry for
193 # - create all tables
194 # - create record in migrations registry
195 # - print / inform the user
197 if migration_number
is None:
198 self
.printer(u
"-> Initializing %s... " % self
.name_for_printing())
201 # auto-set at latest migration number
202 self
.create_new_migration_record()
204 self
.printer(u
"done.\n")
205 self
.set_current_migration()
208 # Run migrations, if appropriate.
209 migrations_to_run
= self
.migrations_to_run()
210 if migrations_to_run
:
212 u
'-> Updating %s:\n' % self
.name_for_printing())
213 for migration_number
, migration_func
in migrations_to_run
:
215 u
' + Running migration %s, "%s"... ' % (
216 migration_number
, migration_func
.func_name
))
217 migration_func(self
.session
)
218 self
.set_current_migration(migration_number
)
219 self
.printer('done.\n')
223 # Otherwise return None. Well it would do this anyway, but
228 class RegisterMigration(object):
230 Tool for registering migrations
234 @RegisterMigration(33)
235 def update_dwarves(database):
238 This will register your migration with the default migration
239 registry. Alternately, to specify a very specific
240 migration_registry, you can pass in that as the second argument.
242 Note, the number of your migration should NEVER be 0 or less than
243 0. 0 is the default "no migrations" state!
245 def __init__(self
, migration_number
, migration_registry
):
246 assert migration_number
> 0, "Migration number must be > 0!"
247 assert migration_number
not in migration_registry
, \
248 "Duplicate migration numbers detected! That's not allowed!"
250 self
.migration_number
= migration_number
251 self
.migration_registry
= migration_registry
253 def __call__(self
, migration
):
254 self
.migration_registry
[self
.migration_number
] = migration
258 def assure_migrations_table_setup(db
):
260 Make sure the migrations table is set up in the database.
262 from mediagoblin
.db
.models
import MigrationData
264 if not MigrationData
.__table
__.exists(db
.bind
):
265 MigrationData
.metadata
.create_all(
266 db
.bind
, tables
=[MigrationData
.__table
__])
269 ##########################
270 # Random utility functions
271 ##########################
274 def atomic_update(table
, query_dict
, update_values
):
275 table
.find(query_dict
).update(update_values
,
276 synchronize_session
=False)
280 def check_media_slug_used(dummy_db
, uploader_id
, slug
, ignore_m_id
):
281 filt
= (MediaEntry
.uploader
== uploader_id
) \
282 & (MediaEntry
.slug
== slug
)
283 if ignore_m_id
is not None:
284 filt
= filt
& (MediaEntry
.id != ignore_m_id
)
285 does_exist
= Session
.query(MediaEntry
.id).filter(filt
).first() is not None
289 def media_entries_for_tag_slug(dummy_db
, tag_slug
):
290 return MediaEntry
.query \
291 .join(MediaEntry
.tags_helper
) \
292 .join(MediaTag
.tag_helper
) \
294 (MediaEntry
.state
== u
'processed')
295 & (Tag
.slug
== tag_slug
))
298 def clean_orphan_tags(commit
=True):
299 """Search for unused MediaTags and delete them"""
300 q1
= Session
.query(Tag
).outerjoin(MediaTag
).filter(MediaTag
.id==None)
303 # The "let the db do all the work" version:
304 # q1 = Session.query(Tag.id).outerjoin(MediaTag).filter(MediaTag.id==None)
305 # q2 = Session.query(Tag).filter(Tag.id.in_(q1))
306 # q2.delete(synchronize_session = False)
311 def check_collection_slug_used(dummy_db
, creator_id
, slug
, ignore_c_id
):
312 filt
= (Collection
.creator
== creator_id
) \
313 & (Collection
.slug
== slug
)
314 if ignore_c_id
is not None:
315 filt
= filt
& (Collection
.id != ignore_c_id
)
316 does_exist
= Session
.query(Collection
.id).filter(filt
).first() is not None
320 if __name__
== '__main__':
321 from mediagoblin
.db
.open import setup_connection_and_db_from_config
323 db
= setup_connection_and_db_from_config({'sql_engine':'sqlite:///mediagoblin.db'})