1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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/>.
18 Utilities for database operations.
20 Some note on migration and indexing tools:
22 We store information about what the state of the database is in the
23 'mediagoblin' document of the 'app_metadata' collection. Keys in that
24 document relevant to here:
26 - 'migration_number': The integer representing the current state of
32 # Imports that other modules might use
33 from pymongo
import ASCENDING
, DESCENDING
34 from pymongo
.errors
import InvalidId
35 from mongokit
import ObjectId
37 from mediagoblin
.db
.indexes
import ACTIVE_INDEXES
, DEPRECATED_INDEXES
45 def add_new_indexes(database
, active_indexes
=ACTIVE_INDEXES
):
47 Add any new indexes to the database.
50 - database: pymongo or mongokit database instance.
51 - active_indexes: indexes to possibly add in the pattern of:
54 'index': [index_foo_goes_here],
56 where 'index' is the index to add and all other options are
57 arguments for collection.create_index.
60 A list of indexes added in form ('collection', 'index_name')
64 for collection_name
, indexes
in active_indexes
.iteritems():
65 collection
= database
[collection_name
]
66 collection_indexes
= collection
.index_information().keys()
68 for index_name
, index_data
in indexes
.iteritems():
69 if not index_name
in collection_indexes
:
70 # Get a copy actually so we don't modify the actual
72 index_data
= copy
.copy(index_data
)
73 index
= index_data
.pop('index')
74 collection
.create_index(
75 index
, name
=index_name
, **index_data
)
77 indexes_added
.append((collection_name
, index_name
))
82 def remove_deprecated_indexes(database
, deprecated_indexes
=DEPRECATED_INDEXES
):
84 Remove any deprecated indexes from the database.
87 - database: pymongo or mongokit database instance.
88 - deprecated_indexes: the indexes to deprecate in the pattern of:
91 'index': [index_foo_goes_here],
94 (... although we really only need the 'identifier' here, as the
95 rest of the information isn't used in this case. But it's kept
96 around so we can remember what it was)
99 A list of indexes removed in form ('collection', 'index_name')
103 for collection_name
, indexes
in deprecated_indexes
.iteritems():
104 collection
= database
[collection_name
]
105 collection_indexes
= collection
.index_information().keys()
107 for index_name
, index_data
in indexes
.iteritems():
108 if index_name
in collection_indexes
:
109 collection
.drop_index(index_name
)
111 indexes_removed
.append((collection_name
, index_name
))
113 return indexes_removed
120 # The default migration registry...
122 # Don't set this yourself! RegisterMigration will automatically fill
123 # this with stuff via decorating methods in migrations.py
125 class MissingCurrentMigration(Exception): pass
131 class RegisterMigration(object):
133 Tool for registering migrations
137 @RegisterMigration(33)
138 def update_dwarves(database):
141 This will register your migration with the default migration
142 registry. Alternately, to specify a very specific
143 migration_registry, you can pass in that as the second argument.
145 Note, the number of your migration should NEVER be 0 or less than
146 0. 0 is the default "no migrations" state!
148 def __init__(self
, migration_number
, migration_registry
=MIGRATIONS
):
149 assert migration_number
> 0, "Migration number must be > 0!"
150 assert not migration_registry
.has_key(migration_number
), \
151 "Duplicate migration numbers detected! That's not allowed!"
153 self
.migration_number
= migration_number
154 self
.migration_registry
= migration_registry
156 def __call__(self
, migration
):
157 self
.migration_registry
[self
.migration_number
] = migration
161 class MigrationManager(object):
163 Migration handling tool.
165 Takes information about a database, lets you update the database
166 to the latest migrations, etc.
168 def __init__(self
, database
, migration_registry
=MIGRATIONS
):
171 - database: database we're going to migrate
172 - migration_registry: where we should find all migrations to
175 self
.database
= database
176 self
.migration_registry
= migration_registry
177 self
._sorted
_migrations
= None
179 def _ensure_current_migration_record(self
):
181 If there isn't a database[u'app_metadata'] mediagoblin entry
182 with the 'current_migration', throw an error.
184 if self
.database_current_migration() is None:
185 raise MissingCurrentMigration(
186 "Tried to call function which requires "
187 "'current_migration' set in database")
190 def sorted_migrations(self
):
192 Sort migrations if necessary and store in self._sorted_migrations
194 if not self
._sorted
_migrations
:
195 self
._sorted
_migrations
= sorted(
196 self
.migration_registry
.items(),
197 # sort on the key... the migration number
198 key
=lambda migration_tuple
: migration_tuple
[0])
200 return self
._sorted
_migrations
202 def latest_migration(self
):
204 Return a migration number for the latest migration, or 0 if
205 there are no migrations.
207 if self
.sorted_migrations
:
208 return self
.sorted_migrations
[-1][0]
210 # If no migrations have been set, we start at 0.
213 def set_current_migration(self
, migration_number
):
215 Set the migration in the database to migration_number
217 # Add the mediagoblin migration if necessary
218 self
.database
[u
'app_metadata'].update(
219 {u
'_id': u
'mediagoblin'},
220 {u
'$set': {u
'current_migration': migration_number
}},
223 def install_migration_version_if_missing(self
):
225 Sets the migration to the latest version if no migration
226 version at all is set.
228 mgoblin_metadata
= self
.database
[u
'app_metadata'].find_one(
229 {u
'_id': u
'mediagoblin'})
230 if not mgoblin_metadata
:
231 latest_migration
= self
.latest_migration()
232 self
.set_current_migration(latest_migration
)
234 def database_current_migration(self
):
236 Return the current migration in the database.
238 mgoblin_metadata
= self
.database
[u
'app_metadata'].find_one(
239 {u
'_id': u
'mediagoblin'})
240 if not mgoblin_metadata
:
243 return mgoblin_metadata
[u
'current_migration']
245 def database_at_latest_migration(self
):
247 See if the database is at the latest migration.
250 current_migration
= self
.database_current_migration()
251 return current_migration
== self
.latest_migration()
253 def migrations_to_run(self
):
255 Get a list of migrations to run still, if any.
257 Note that calling this will set your migration version to the
258 latest version if it isn't installed to anything yet!
260 self
._ensure
_current
_migration
_record
()
262 db_current_migration
= self
.database_current_migration()
265 (migration_number
, migration_func
)
266 for migration_number
, migration_func
in self
.sorted_migrations
267 if migration_number
> db_current_migration
]
269 def migrate_new(self
, pre_callback
=None, post_callback
=None):
273 Includes two optional args:
274 - pre_callback: if called, this is a callback on something to
275 run pre-migration. Takes (migration_number, migration_func)
277 - pre_callback: if called, this is a callback on something to
278 run post-migration. Takes (migration_number, migration_func)
281 # If we aren't set to any version number, presume we're at the
282 # latest (which means we'll do nothing here...)
283 self
.install_migration_version_if_missing()
285 for migration_number
, migration_func
in self
.migrations_to_run():
287 pre_callback(migration_number
, migration_func
)
288 migration_func(self
.database
)
289 self
.set_current_migration(migration_number
)
291 post_callback(migration_number
, migration_func
)