Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
sugar-datastore/src/carquinyol/datastore.py /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
564 lines (477 sloc)
20.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (C) 2008, One Laptop Per Child | |
| # Based on code Copyright (C) 2007, ObjectRealms, LLC | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 2 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program; if not, write to the Free Software | |
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
| # pylint fails on @debian's arguments | |
| # pylint: disable=C0322 | |
| import logging | |
| import uuid | |
| import time | |
| import os | |
| import shutil | |
| import subprocess | |
| import tempfile | |
| import dbus | |
| import dbus.service | |
| from gi.repository import GLib | |
| from sugar3 import mime | |
| from carquinyol import layoutmanager | |
| from carquinyol import migration | |
| from carquinyol.layoutmanager import MAX_QUERY_LIMIT | |
| from carquinyol.metadatastore import MetadataStore | |
| from carquinyol.indexstore import IndexStore | |
| from carquinyol.filestore import FileStore | |
| from carquinyol.optimizer import Optimizer | |
| # the name used by the logger | |
| DS_SERVICE = "org.laptop.sugar.DataStore" | |
| DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore" | |
| DS_OBJECT_PATH = "/org/laptop/sugar/DataStore" | |
| MIN_INDEX_FREE_BYTES = 1024 * 1024 * 5 | |
| logger = logging.getLogger('datastore') | |
| class DataStore(dbus.service.Object): | |
| """D-Bus API and logic for connecting all the other components. | |
| """ | |
| def __init__(self, **options): | |
| bus_name = dbus.service.BusName(DS_SERVICE, | |
| bus=dbus.SessionBus(), | |
| replace_existing=False, | |
| allow_replacement=False) | |
| dbus.service.Object.__init__(self, bus_name, DS_OBJECT_PATH) | |
| migrated, initiated = self._open_layout() | |
| self._metadata_store = MetadataStore() | |
| self._file_store = FileStore() | |
| self._optimizer = Optimizer(self._file_store, self._metadata_store) | |
| self._index_store = IndexStore() | |
| self._index_updating = False | |
| root_path = layoutmanager.get_instance().get_root_path() | |
| self._cleanflag = os.path.join(root_path, 'ds_clean') | |
| if initiated: | |
| logger.debug('Initiate datastore') | |
| self._rebuild_index() | |
| self._index_store.flush() | |
| self._mark_clean() | |
| return | |
| if migrated: | |
| self._rebuild_index() | |
| self._mark_clean() | |
| return | |
| rebuild = False | |
| stat = os.statvfs(root_path) | |
| da = stat.f_bavail * stat.f_bsize | |
| if not self._index_store.index_updated: | |
| logger.warn('Index is not up-to-date') | |
| rebuild = True | |
| elif not os.path.exists(self._cleanflag): | |
| logger.warn('DS state is not clean') | |
| rebuild = True | |
| elif da < MIN_INDEX_FREE_BYTES: | |
| logger.warn('Disk space tight for index') | |
| rebuild = True | |
| if rebuild: | |
| logger.warn('Trigger index rebuild') | |
| self._rebuild_index() | |
| else: | |
| # fast path | |
| try: | |
| self._index_store.open_index() | |
| except BaseException: | |
| logger.exception('Failed to open index') | |
| # try... | |
| self._rebuild_index() | |
| self._mark_clean() | |
| return | |
| def _mark_clean(self): | |
| try: | |
| f = open(self._cleanflag, 'w') | |
| os.fsync(f.fileno()) | |
| f.close() | |
| except BaseException: | |
| logger.exception("Could not mark the datastore clean") | |
| def _mark_dirty(self): | |
| try: | |
| os.remove(self._cleanflag) | |
| except BaseException: | |
| pass | |
| def _open_layout(self): | |
| """Open layout manager, check version of data store on disk and | |
| migrate if necessary. | |
| Returns a pair of booleans. For the first, True if migration was done | |
| and an index rebuild is required. For the second, True if datastore was | |
| just initiated. | |
| """ | |
| layout_manager = layoutmanager.get_instance() | |
| if layout_manager.is_empty(): | |
| layout_manager.set_version(layoutmanager.CURRENT_LAYOUT_VERSION) | |
| return False, True | |
| old_version = layout_manager.get_version() | |
| if old_version == layoutmanager.CURRENT_LAYOUT_VERSION: | |
| return False, False | |
| if old_version == 0: | |
| migration.migrate_from_0() | |
| layout_manager.set_version(layoutmanager.CURRENT_LAYOUT_VERSION) | |
| return True, False | |
| def _rebuild_index(self): | |
| """Remove and recreate index.""" | |
| self._index_store.close_index() | |
| self._index_store.remove_index() | |
| # rebuild the index in tmpfs to better handle ENOSPC | |
| temp_index_path = tempfile.mkdtemp(prefix='sugar-datastore-index-') | |
| logger.debug('Rebuilding index in %s' % temp_index_path) | |
| self._index_store.open_index(temp_path=temp_index_path) | |
| self._update_index() | |
| self._index_store.close_index() | |
| on_disk = False | |
| # can we fit the index on disk? get disk usage in bytes... | |
| index_du = subprocess.check_output(['/usr/bin/du', '-bs', | |
| temp_index_path]) | |
| index_du = int(index_du.split(b'\t')[0]) | |
| # disk available, in bytes | |
| stat = os.statvfs(temp_index_path) | |
| da = stat.f_bavail * stat.f_bsize | |
| if da > (index_du * 1.2) and da > MIN_INDEX_FREE_BYTES: | |
| # 1.2 due to 20% room for growth | |
| logger.debug('Attempting to move tempfs index to disk') | |
| # move to internal disk | |
| try: | |
| index_path = layoutmanager.get_instance().get_index_path() | |
| if os.path.exists(index_path): | |
| shutil.rmtree(index_path) | |
| shutil.copytree(temp_index_path, index_path) | |
| shutil.rmtree(temp_index_path) | |
| on_disk = True | |
| except Exception: | |
| logger.exception('Error copying tempfs index to disk,' | |
| 'revert to using tempfs index.') | |
| else: | |
| logger.warn("Not enough disk space, using tempfs index") | |
| if on_disk: | |
| self._index_store.open_index() | |
| else: | |
| self._index_store.open_index(temp_path=temp_index_path) | |
| def _update_index(self): | |
| """Find entries that are not yet in the index and add them.""" | |
| uids = layoutmanager.get_instance().find_all() | |
| logger.debug('Going to update the index with object_ids %r', | |
| uids) | |
| self._index_updating = True | |
| GLib.idle_add(lambda: self.__update_index_cb(uids), | |
| priority=GLib.PRIORITY_LOW) | |
| def __update_index_cb(self, uids): | |
| if uids: | |
| uid = uids.pop() | |
| logger.debug('Updating entry %r in index. %d to go.', uid, | |
| len(uids)) | |
| if not self._index_store.contains(uid): | |
| try: | |
| update_metadata = False | |
| props = self._metadata_store.retrieve(uid) | |
| if 'filesize' not in props: | |
| path = self._file_store.get_file_path(uid) | |
| if os.path.exists(path): | |
| props['filesize'] = os.stat(path).st_size | |
| update_metadata = True | |
| if 'timestamp' not in props: | |
| props['timestamp'] = str(int(time.time())) | |
| update_metadata = True | |
| if 'creation_time' not in props: | |
| if 'ctime' in props: | |
| try: | |
| props['creation_time'] = time.mktime( | |
| time.strptime( | |
| props['ctime'], | |
| migration.DATE_FORMAT)) | |
| except (TypeError, ValueError): | |
| pass | |
| if 'creation_time' not in props: | |
| props['creation_time'] = props['timestamp'] | |
| update_metadata = True | |
| if update_metadata: | |
| self._metadata_store.store(uid, props) | |
| self._index_store.store(uid, props) | |
| except Exception: | |
| logger.exception('Error processing %r', uid) | |
| logger.warn('Will attempt to delete corrupt entry %r', | |
| uid) | |
| try: | |
| # self.delete(uid) only works on well-formed | |
| # entries :-/ | |
| entry_path = \ | |
| layoutmanager.get_instance().get_entry_path(uid) | |
| shutil.rmtree(entry_path) | |
| except Exception: | |
| logger.exception('Error deleting corrupt entry %r', | |
| uid) | |
| if not uids: | |
| self._index_store.flush() | |
| self._index_updating = False | |
| logger.debug('Finished updating index.') | |
| return False | |
| else: | |
| return True | |
| def _create_completion_cb(self, async_cb, async_err_cb, uid, exc=None): | |
| logger.debug('_create_completion_cb(%r, %r, %r, %r)', async_cb, | |
| async_err_cb, uid, exc) | |
| if exc is not None: | |
| async_err_cb(exc) | |
| return | |
| self.Created(uid) | |
| self._optimizer.optimize(uid) | |
| logger.debug('created %s', uid) | |
| self._mark_clean() | |
| async_cb(uid) | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='a{sv}sb', | |
| out_signature='s', | |
| async_callbacks=('async_cb', 'async_err_cb'), | |
| byte_arrays=True) | |
| def create(self, props, file_path, transfer_ownership, | |
| async_cb, async_err_cb): | |
| uid = str(uuid.uuid4()) | |
| logger.debug('datastore.create %r', uid) | |
| self._mark_dirty() | |
| if not props.get('timestamp', ''): | |
| props['timestamp'] = int(time.time()) | |
| # FIXME: Support for the deprecated ctime property. Remove in 0.92. | |
| if 'ctime' in props: | |
| try: | |
| props['creation_time'] = time.mktime(time.strptime( | |
| migration.DATE_FORMAT, props['ctime'])) | |
| except (TypeError, ValueError): | |
| pass | |
| if 'creation_time' not in props: | |
| props['creation_time'] = props['timestamp'] | |
| if os.path.exists(file_path): | |
| stat = os.stat(file_path) | |
| props['filesize'] = stat.st_size | |
| else: | |
| props['filesize'] = 0 | |
| self._metadata_store.store(uid, props) | |
| self._index_store.store(uid, props) | |
| self._file_store.store( | |
| uid, file_path, transfer_ownership, | |
| lambda * args: self._create_completion_cb(async_cb, | |
| async_err_cb, | |
| uid, * args)) | |
| @dbus.service.signal(DS_DBUS_INTERFACE, signature="s") | |
| def Created(self, uid): | |
| pass | |
| def _update_completion_cb(self, async_cb, async_err_cb, uid, exc=None): | |
| logger.debug('_update_completion_cb() called with %r / %r, exc %r', | |
| async_cb, async_err_cb, exc) | |
| if exc is not None: | |
| async_err_cb(exc) | |
| return | |
| self.Updated(uid) | |
| self._optimizer.optimize(uid) | |
| logger.debug('updated %s', uid) | |
| self._mark_clean() | |
| async_cb() | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='sa{sv}sb', | |
| out_signature='', | |
| async_callbacks=('async_cb', 'async_err_cb'), | |
| byte_arrays=True) | |
| def update(self, uid, props, file_path, transfer_ownership, | |
| async_cb, async_err_cb): | |
| logger.debug('datastore.update %r', uid) | |
| self._mark_dirty() | |
| if not props.get('timestamp', ''): | |
| props['timestamp'] = int(time.time()) | |
| # FIXME: Support for the deprecated ctime property. Remove in 0.92. | |
| if 'ctime' in props: | |
| try: | |
| props['creation_time'] = time.mktime(time.strptime( | |
| migration.DATE_FORMAT, props['ctime'])) | |
| except (TypeError, ValueError): | |
| pass | |
| if 'creation_time' not in props: | |
| props['creation_time'] = props['timestamp'] | |
| if file_path: | |
| # Empty file_path means skipping storage stage, see filestore.py | |
| # TODO would be more useful to update filesize after real file save | |
| if os.path.exists(file_path): | |
| stat = os.stat(file_path) | |
| props['filesize'] = stat.st_size | |
| else: | |
| props['filesize'] = 0 | |
| self._metadata_store.store(uid, props) | |
| self._index_store.store(uid, props) | |
| if os.path.exists(self._file_store.get_file_path(uid)) and \ | |
| (not file_path or os.path.exists(file_path)): | |
| self._optimizer.remove(uid) | |
| self._file_store.store( | |
| uid, file_path, transfer_ownership, | |
| lambda * args: self._update_completion_cb(async_cb, | |
| async_err_cb, | |
| uid, * args)) | |
| @dbus.service.signal(DS_DBUS_INTERFACE, signature="s") | |
| def Updated(self, uid): | |
| pass | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='a{sv}as', | |
| out_signature='aa{sv}u') | |
| def find(self, query, properties): | |
| logger.debug('datastore.find %r', query) | |
| t = time.time() | |
| if not self._index_updating: | |
| try: | |
| uids, count = self._index_store.find(query) | |
| uids = [uid.decode() for uid in uids] | |
| except Exception: | |
| logger.exception('Failed to query index, will rebuild') | |
| self._rebuild_index() | |
| if self._index_updating: | |
| logger.warning('Index updating, returning all entries') | |
| return self._find_all(query, properties) | |
| entries = [] | |
| for uid in uids: | |
| entry_path = layoutmanager.get_instance().get_entry_path(uid) | |
| if not os.path.exists(entry_path): | |
| logger.warning( | |
| 'Inconsistency detected, returning all entries') | |
| self._rebuild_index() | |
| return self._find_all(query, properties) | |
| metadata = self._metadata_store.retrieve(uid, properties) | |
| self._fill_internal_props(metadata, uid, properties) | |
| entries.append(metadata) | |
| logger.debug('find(): %r', time.time() - t) | |
| return entries, count | |
| def _find_all(self, query, properties): | |
| uids = layoutmanager.get_instance().find_all() | |
| count = len(uids) | |
| offset = query.get('offset', 0) | |
| limit = query.get('limit', MAX_QUERY_LIMIT) | |
| uids = uids[offset:offset + limit] | |
| entries = [] | |
| for uid in uids: | |
| metadata = self._metadata_store.retrieve(uid, properties) | |
| self._fill_internal_props(metadata, uid, properties) | |
| entries.append(metadata) | |
| return entries, count | |
| def _fill_internal_props(self, metadata, uid, names=None): | |
| """Fill in internal / computed properties in metadata | |
| Properties are only set if they appear in names or if names is | |
| empty. | |
| """ | |
| if not names or 'uid' in names: | |
| metadata['uid'] = uid | |
| if not names or 'filesize' in names: | |
| file_path = self._file_store.get_file_path(uid) | |
| if os.path.exists(file_path): | |
| stat = os.stat(file_path) | |
| metadata['filesize'] = str(stat.st_size) | |
| else: | |
| metadata['filesize'] = '0' | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='a{sv}', | |
| out_signature='as') | |
| def find_ids(self, query): | |
| if not self._index_updating: | |
| try: | |
| return self._index_store.find(query)[0] | |
| except Exception: | |
| logger.error('Failed to query index, will rebuild') | |
| self._rebuild_index() | |
| return [] | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='s', | |
| out_signature='s', | |
| sender_keyword='sender') | |
| def get_filename(self, uid, sender=None): | |
| logger.debug('datastore.get_filename %r', uid) | |
| user_id = dbus.Bus().get_unix_user(sender) | |
| extension = self._get_extension(uid) | |
| return self._file_store.retrieve(uid, user_id, extension) | |
| def _get_extension(self, uid): | |
| mime_type = self._metadata_store.get_property(uid, 'mime_type') | |
| if mime_type is None or not mime_type: | |
| return '' | |
| return mime.get_primary_extension(mime_type) | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='s', | |
| out_signature='a{sv}') | |
| def get_properties(self, uid): | |
| logger.debug('datastore.get_properties %r', uid) | |
| metadata = self._metadata_store.retrieve(uid) | |
| self._fill_internal_props(metadata, uid) | |
| return metadata | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='sa{sv}', | |
| out_signature='as') | |
| def get_uniquevaluesfor(self, propertyname, query=None): | |
| if propertyname != 'activity': | |
| raise ValueError('Only ''activity'' is a supported property name') | |
| if query: | |
| raise ValueError('The query parameter is not supported') | |
| if not self._index_updating: | |
| return self._index_store.get_activities() | |
| else: | |
| logger.warning('Index updating, returning an empty list') | |
| return [] | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature='s', | |
| out_signature='') | |
| def delete(self, uid): | |
| self._mark_dirty() | |
| try: | |
| entry_path = layoutmanager.get_instance().get_entry_path(uid) | |
| self._optimizer.remove(uid) | |
| self._index_store.delete(uid) | |
| self._file_store.delete(uid) | |
| self._metadata_store.delete(uid) | |
| # remove the dirtree | |
| shutil.rmtree(entry_path) | |
| try: | |
| # will remove the hashed dir if nothing else is there | |
| os.removedirs(os.path.dirname(entry_path)) | |
| except BaseException: | |
| pass | |
| except BaseException: | |
| logger.exception('Exception deleting entry') | |
| raise | |
| self.Deleted(uid) | |
| logger.debug('deleted %s', uid) | |
| self._mark_clean() | |
| @dbus.service.signal(DS_DBUS_INTERFACE, signature="s") | |
| def Deleted(self, uid): | |
| pass | |
| def stop(self): | |
| """shutdown the service""" | |
| self._index_store.close_index() | |
| self.Stopped() | |
| @dbus.service.signal(DS_DBUS_INTERFACE) | |
| def Stopped(self): | |
| pass | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature="sa{sv}", | |
| out_signature='s') | |
| def mount(self, uri, options=None): | |
| return '' | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature="", | |
| out_signature="aa{sv}") | |
| def mounts(self): | |
| return [{'id': 1}] | |
| @dbus.service.method(DS_DBUS_INTERFACE, | |
| in_signature="s", | |
| out_signature="") | |
| def unmount(self, mountpoint_id): | |
| pass | |
| @dbus.service.signal(DS_DBUS_INTERFACE, signature="a{sv}") | |
| def Mounted(self, descriptior): | |
| pass | |
| @dbus.service.signal(DS_DBUS_INTERFACE, signature="a{sv}") | |
| def Unmounted(self, descriptor): | |
| pass |