diff options
Diffstat (limited to 'extensions/cpsection/updater/model.py')
-rwxr-xr-x | extensions/cpsection/updater/model.py | 472 |
1 files changed, 244 insertions, 228 deletions
diff --git a/extensions/cpsection/updater/model.py b/extensions/cpsection/updater/model.py index 7a4a908..489cfa5 100755 --- a/extensions/cpsection/updater/model.py +++ b/extensions/cpsection/updater/model.py @@ -1,5 +1,5 @@ -#!/usr/bin/python # Copyright (C) 2009, Sugar Labs +# Copyright (C) 2009, Tomeu Vizoso # # 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 @@ -14,263 +14,279 @@ # 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 -"""Sugar bundle updater: model. +'''Sugar bundle updater: model. This module implements the non-GUI portions of the bundle updater, including list of installed bundls, whether updates are needed, and the URL at which to find the bundle updated. - -`UpdateList` inherits from `gtk.ListStore` in order to work closely with the -view pane. This module requires `gtk`. -""" +''' import os -import tempfile -import locale import logging -import urllib -from gettext import gettext as _ +import tempfile +from urlparse import urlparse import traceback import gobject -import gtk +import gio -from sugar.bundle.activitybundle import ActivityBundle +from sugar import env from sugar.datastore import datastore +from sugar.bundle.activitybundle import ActivityBundle from jarabe.model import bundleregistry + from backends import aslo -"""List of columns in the `UpdateList`.""" -BUNDLE_ID, \ - BUNDLE, \ - ICON, \ - NAME, \ - CURRENT_VERSION, \ - UPDATE_VERSION, \ - UPDATE_SIZE, \ - UPDATE_URL, \ - DESCRIPTION, \ - UPDATE_SELECTED, \ - UPDATE_AVAILABLE, \ - IS_HEADER = xrange(12) - -class UpdateList(gtk.ListStore): - """Model which provides backing storage for the BUNDLE list treeview.""" - - __gproperties__ = { - 'is_valid': (gobject.TYPE_BOOLEAN, 'is valid', - 'true iff the UpdateList has been properly refreshed', - False, gobject.PARAM_READABLE), + +class UpdateModel(gobject.GObject): + __gtype_name__ = 'SugarUpdateModel' + + __gsignals__ = { + 'progress': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([int, str, float, int])), } + ACTION_CHECKING = 0 + ACTION_UPDATING = 1 + ACTION_DOWNLOADING = 2 + def __init__(self): - logging.debug('STARTUP: Loading the bundle updater') + gobject.GObject.__init__(self) + + self.updates = None + self._bundles_to_check = None + self._bundles_to_update = None + self._total_bundles_to_update = 0 + self._downloader = None + + def check_updates(self): + self.updates = [] + self._bundles_to_check = \ + [bundle for bundle in bundleregistry.get_registry()] + self._check_next_update() + + def _check_next_update(self): + total = len(bundleregistry.get_registry()) + current = total - len(self._bundles_to_check) + + bundle = self._bundles_to_check.pop() + self.emit('progress', UpdateModel.ACTION_CHECKING, bundle.get_name(), + current, total) + + aslo.fetch_update_info(bundle, self.__check_completed_cb) + + def __check_completed_cb(self, bundle, version, link, size, error_message): + if error_message is not None: + logging.error('Error getting update information from server:\n' + '%s' % error_message) + + if version is not None and version > bundle.get_activity_version(): + self.updates.append(BundleUpdate(bundle, version, link, size)) + + if self._bundles_to_check: + gobject.idle_add(self._check_next_update) + else: + total = len(bundleregistry.get_registry()) + if bundle is None: + name = '' + else: + name = bundle.get_name() + self.emit('progress', UpdateModel.ACTION_CHECKING, name, total, + total) + + def update(self, bundle_ids): + self._bundles_to_update = [] + for bundle_update in self.updates: + if bundle_update.bundle.get_bundle_id() in bundle_ids: + self._bundles_to_update.append(bundle_update) + + self._total_bundles_to_update = len(self._bundles_to_update) + self._download_next_update() + + def _download_next_update(self): + bundle_update = self._bundles_to_update.pop() - gtk.ListStore.__init__(self, - str, object, gtk.gdk.Pixbuf, str, - long, long, long, str, - str, bool, bool, bool) + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 2 - self._cancel = False - self._is_valid = True - self._registry = bundleregistry.get_registry() - self._steps_count = 0 - self._steps_total = 0 - self._progress_cb = None + self.emit('progress', UpdateModel.ACTION_DOWNLOADING, + bundle_update.bundle.get_name(), current, total) - def refresh_list(self, progress_callback=lambda n, extra: None, - clear_cache=True): - self._cancel = False - self._progress_cb = progress_callback - self._progress_cb(None, _('Looking for local actvities...')) + self._downloader = _Downloader(bundle_update) + self._downloader.connect('progress', self.__downloader_progress_cb) + self._downloader.connect('error', self.__downloader_error_cb) - self.clear() - self._steps_total = len([i for i in self._registry]) - self._steps_count = 0 + def __downloader_progress_cb(self, downloader, progress): + logging.debug('__downloader_progress_cb %r' % progress) + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 2 + progress - row_map = {} + self.emit('progress', UpdateModel.ACTION_DOWNLOADING, + self._downloader.bundle_update.bundle.get_name(), + current, total) - for bundle in self._registry: - self._make_progress(_('Checking %s...') % bundle.get_name()) + if progress == 1: + self._install_update(self._downloader.bundle_update, + self._downloader.get_local_file_path()) + self._downloader = None - if self._cancel: - break # Cancel bundle refresh + def __downloader_error_cb(self, downloader, error_message): + logging.error('Error downloading update:\n%s' % error_message) - row = [None, None, None, None, - 0, 0, 0, None, - None, True, False, False] - row[BUNDLE] = bundle - row[BUNDLE_ID] = bundle.get_bundle_id() + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 + self.emit('progress', UpdateModel.ACTION_UPDATING, '', current, total) - if self._refresh_row(row): - row_map[row[BUNDLE_ID]] = self.get_path(self.append(row)) + if self._bundles_to_update: + # do it in idle so the UI has a chance to refresh + gobject.idle_add(self._download_next_update) - def cancel(self): - self._cancel = True + def _install_update(self, bundle_update, local_file_path): - def _refresh_row(self, row): - logging.debug('Looking for %s' % row[BUNDLE].get_name()) + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 1 + self.emit('progress', UpdateModel.ACTION_UPDATING, + bundle_update.bundle.get_name(), + current, total) + + # TODO: Should we first expand the zip async so we can provide progress + # and only then copy to the journal? + jobject = datastore.create() + try: + title = '%s-%s' % (bundle_update.bundle.get_name(), + bundle_update.version) + jobject.metadata['title'] = title + jobject.metadata['mime_type'] = ActivityBundle.MIME_TYPE + jobject.file_path = local_file_path + datastore.write(jobject, transfer_ownership=True) + finally: + jobject.destroy() + + current += 1 + self.emit('progress', UpdateModel.ACTION_UPDATING, + bundle_update.bundle.get_name(), + current, total) + + if self._bundles_to_update: + # do it in idle so the UI has a chance to refresh + gobject.idle_add(self._download_next_update) + + def get_total_bundles_to_update(self): + return self._total_bundles_to_update + + +class BundleUpdate(object): + + def __init__(self, bundle, version, link, size): + self.bundle = bundle + self.version = version + self.link = link + self.size = size + + +class _Downloader(gobject.GObject): + _CHUNK_SIZE = 10240 # 10K + __gsignals__ = { + 'progress': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([float])), + 'error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, bundle_update): + gobject.GObject.__init__(self) + + self.bundle_update = bundle_update + self._input_stream = None + self._output_stream = None + self._pending_buffers = [] + self._input_file = gio.File(bundle_update.link) + self._output_file = None + self._downloaded_size = 0 + + self._input_file.read_async(self.__file_read_async_cb) + + def __file_read_async_cb(self, gfile, result): try: - new_ver, new_url, new_size = aslo.fetch_update_info(row[BUNDLE]) - except Exception: - logging.warning('Failure updating %s\n%s' % \ - (row[BUNDLE].get_name(), traceback.format_exc())) - return False - - row[CURRENT_VERSION] = row[BUNDLE].get_activity_version() - row[UPDATE_VERSION] = long(new_ver) - - if row[CURRENT_VERSION] >= row[UPDATE_VERSION]: - logging.debug('Skip %s update' % row[BUNDLE].get_name()) - return False - - row[ICON] = gtk.gdk.pixbuf_new_from_file_at_size( - row[BUNDLE].get_icon(), 32, 32) - row[NAME] = row[BUNDLE].get_name() - row[UPDATE_URL] = new_url - row[UPDATE_AVAILABLE] = True - row[UPDATE_SIZE] = new_size - row[DESCRIPTION] = \ - _('From version %(current)d to %(new)s (Size: %(size)s)') % \ - {'current': row[CURRENT_VERSION], - 'new': row[UPDATE_VERSION], - 'size': _humanize_size(row[UPDATE_SIZE])} - row[UPDATE_SELECTED] = True - - return True - - def install_updates(self, progress_cb=(lambda n, row: None)): - self._cancel = False - self._progress_cb = progress_cb - self._steps_total = len([0 for row in self if row[UPDATE_SELECTED]]) * 2 - self._steps_count = 0 - - installed = 0 - - for row in self: - if self._cancel: - return installed - if row[IS_HEADER]: - continue - if not row[UPDATE_SELECTED]: - continue - - logging.debug('Downloading %s from %s' % \ - (row[NAME], row[UPDATE_URL])) - self._make_progress(_('Downloading %s...') % row[NAME]) - - fd, xofile = tempfile.mkstemp(suffix='.xo') - os.close(fd) - try: - try: - urllib.urlretrieve(row[UPDATE_URL], xofile) - except Exception, e: - logging.warning("Can't download %s: %s" % \ - (row[UPDATE_URL], e)) - continue - - if self._cancel: - return installed - - logging.debug('Installing %s' % row[NAME]) - self._make_progress(_('Installing %s...') % row[NAME]) - - jobject = datastore.create() - jobject.metadata['title'] = \ - '%s-%s' % (row[NAME], row[UPDATE_VERSION]) - jobject.metadata['mime_type'] = ActivityBundle.MIME_TYPE - jobject.file_path = xofile - datastore.write(jobject, transfer_ownership=True) - - installed += 1 - finally: - if os.path.exists(xofile): - os.unlink(xofile) - - return installed - -############################################################################### - - def _make_progress(self, msg=None): #FIXME needs better name - """Helper function to do progress update.""" - self._steps_count += 1 - self._progress_cb(float(self._steps_count)/self._steps_total, msg) - - def _sum_rows(self, row_func): - """Sum the values returned by row_func called on all non-header - rows.""" - return sum(row_func(r) for r in self if not r[IS_HEADER]) - -############################################################################### - - def updates_available(self): - """Return the number of updates available. - - Updated by `refresh`.""" - return self._sum_rows(lambda r: 1 if r[UPDATE_AVAILABLE] else 0) - - def updates_selected(self): - """Return the number of updates selected.""" - return self._sum_rows(lambda r: - 1 if r[UPDATE_AVAILABLE] and r[UPDATE_SELECTED] else 0) - - def updates_size(self): - """Returns the size (in bytes) of the selected updates available. - - Updated by `refresh`.""" - return self._sum_rows(lambda r: - r[UPDATE_SIZE] if r[UPDATE_AVAILABLE] and \ - r[UPDATE_SELECTED] else 0) - def is_valid(self): - """The UpdateList is invalidated before it is refreshed, and when - the group information is modified without refreshing.""" - return self._is_valid - -############################################################################### -# Utility Funtions - -def _humanize_size(bytes_): - """ - Convert a given size in bytes to a nicer better readable unit - """ - if bytes_ == 0: - # TRANS: download size is 0 - return _('None') - elif bytes_ < 1024: - # TRANS: download size of very small updates - return _('1 KB') - elif bytes_ < 1024 * 1024: - # TRANS: download size of small updates, e.g. '250 KB' - return locale.format(_('%.0f KB'), bytes_ / 1024) - else: - # TRANS: download size of updates, e.g. '2.3 MB' - return locale.format(_('%.1f MB'), bytes_ / 1024 / 1024) - -def print_available(ul):#FIXME this should onlu return available updates - def opt(x): - if x is None or x == '': - return '' - return ': %s' % x - for row in ul: - if row[IS_HEADER]: - print row[NAME] + opt(row[DESCRIPTION]) + self._input_stream = self._input_file.read_finish(result) + except: + self.emit('error', traceback.format_exc()) + return + + temp_file_path = self._get_temp_file_path(self.bundle_update.link) + self._output_file = gio.File(temp_file_path) + self._output_stream = self._output_file.create() + + self._input_stream.read_async(self._CHUNK_SIZE, self.__read_async_cb, + gobject.PRIORITY_LOW) + + def __read_async_cb(self, input_stream, result): + data = input_stream.read_finish(result) + + if data is None: + # TODO + pass + elif not data: + logging.debug('closing input stream') + self._input_stream.close() + self._check_if_finished_writing() else: - print '*', row[NAME] + opt(row[DESCRIPTION]) - print - #print _('%(number)d updates available. Size: %(size)s') % \ - # { 'number': ul.updates_available(), - # 'size': _humanize_size(ul.updates_size()) } - -############################################################################### -# Self-test code. -def _main(): - """Self-test.""" - update_list = UpdateList() - update_list.refresh_list() - print_available(update_list) - update_list.install_updates() - -if __name__ == '__main__': - _main() + self._pending_buffers.append(data) + self._input_stream.read_async(self._CHUNK_SIZE, + self.__read_async_cb, + gobject.PRIORITY_LOW) + + self._write_next_buffer() + + def __write_async_cb(self, output_stream, result, user_data): + count = output_stream.write_finish(result) + + self._downloaded_size += count + progress = self._downloaded_size / float(self.bundle_update.size) + self.emit('progress', progress) + + self._check_if_finished_writing() + + if self._pending_buffers: + self._write_next_buffer() + + def _write_next_buffer(self): + if self._pending_buffers and not self._output_stream.has_pending(): + data = self._pending_buffers.pop(0) + # TODO: we pass the buffer as user_data because of + # http://bugzilla.gnome.org/show_bug.cgi?id=564102 + self._output_stream.write_async(data, self.__write_async_cb, + gobject.PRIORITY_LOW, + user_data=data) + + def _get_temp_file_path(self, uri): + # TODO: Should we use the HTTP headers for the file name? + scheme_, netloc_, path, params_, query_, fragment_ = \ + urlparse(uri) + path = os.path.basename(path) + + base_name, extension_ = os.path.splitext(path) + fd, file_path = tempfile.mkstemp(dir=env.get_user_activities_path(), + prefix=base_name, suffix='.xo') + os.close(fd) + os.unlink(file_path) + + return file_path + + def get_local_file_path(self): + return self._output_file.get_path() + + def _check_if_finished_writing(self): + if not self._pending_buffers and \ + not self._output_stream.has_pending() and \ + self._input_stream.is_closed(): + + logging.debug('closing output stream') + self._output_stream.close() + + self.emit('progress', 1.0) |