[Sugar-devel] [PATCH browse] Bring back download functionality using WebKit

Simon Schampijer simon at schampijer.de
Thu Jan 12 15:55:39 EST 2012


This brings back the download functionality to
Browse using WebKit, it handles the cases when you
click on a link and the Browser is not setup to view
this file.

Requesting a download through Palettes is still to
come.

The code is inspired by the download code in Surf.

Signed-off-by: Simon Schampijer <simon at laptop.org>
---
 browser.py         |   14 ++
 downloadmanager.py |  379 ++++++++++------------------------------------------
 webactivity.py     |    9 +-
 3 files changed, 88 insertions(+), 314 deletions(-)

diff --git a/browser.py b/browser.py
index 2d65237..83cc7ea 100644
--- a/browser.py
+++ b/browser.py
@@ -35,6 +35,7 @@ from sugar3.graphics.icon import Icon
 
 from widgets import BrowserNotebook
 import globalhistory
+import downloadmanager
 
 _ZOOM_AMOUNT = 0.1
 _LIBRARY_PATH = '/usr/share/library-common/index.html'
@@ -389,6 +390,9 @@ class Browser(WebKit.WebView):
         self._global_history = globalhistory.get_global_history()
         self.connect('notify::load-status', self.__load_status_changed_cb)
         self.connect('notify::title', self.__title_changed_cb)
+        self.connect('download-requested', self.__download_requested_cb)
+        self.connect('mime-type-policy-decision-requested',
+                     self.__mime_type_policy_cb)
 
     def get_history(self):
         """Return the browsing history of this browser."""
@@ -478,6 +482,16 @@ class Browser(WebKit.WebView):
                 title = unicode(title, 'utf-8')
             self._global_history.set_page_title(uri, title)
 
+    def __mime_type_policy_cb(self, webview, frame, request, mimetype,
+                              policy_decision):
+        if not self.can_show_mime_type(mimetype):
+            policy_decision.download()
+        return True
+
+    def __download_requested_cb(self, browser, download):
+        downloadmanager.add_download(download, browser)
+        return True
+
 
 class PopupDialog(Gtk.Window):
     def __init__(self):
diff --git a/downloadmanager.py b/downloadmanager.py
index af22df4..eeeee6d 100644
--- a/downloadmanager.py
+++ b/downloadmanager.py
@@ -18,44 +18,23 @@
 import os
 import logging
 from gettext import gettext as _
-import time
 import tempfile
+import dbus
 
 from gi.repository import Gtk
-import hulahop
-import xpcom
-from xpcom.nsError import *
-from xpcom import components
-from xpcom.components import interfaces
-from xpcom.server.factory import Factory
+from gi.repository import WebKit
 
 from sugar3.datastore import datastore
 from sugar3 import profile
 from sugar3 import mime
 from sugar3.graphics.alert import Alert, TimeoutAlert
 from sugar3.graphics.icon import Icon
-from sugar3.graphics import style
 from sugar3.activity import activity
-# #3903 - this constant can be removed and assumed to be 1 when dbus-python
-# 0.82.3 is the only version used
-import dbus
-if dbus.version >= (0, 82, 3):
-    DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1
-else:
-    DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000
-
-NS_BINDING_ABORTED = 0x804b0002             # From nsNetError.h
-NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020  # From nsURILoader.h
 
 DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
 DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
 DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
 
-_MIN_TIME_UPDATE = 5        # In seconds
-_MIN_PERCENT_UPDATE = 10
-
-_MAX_DELTA_CACHE_TIME = 86400  # In seconds
-
 _active_downloads = []
 _dest_to_window = {}
 
@@ -70,78 +49,22 @@ def num_downloads():
 
 def remove_all_downloads():
     for download in _active_downloads:
-        download.cancelable.cancel(NS_ERROR_FAILURE)
+        download.cancel()
         if download.dl_jobject is not None:
             datastore.delete(download.dl_jobject.object_id)
         download.cleanup()
 
-def remove_old_parts():
-    temp_path = os.path.join(activity.get_activity_root(), 'instance')
-    if os.path.exists(temp_path):
-        for file in os.listdir(temp_path):
-            file_full_path = os.path.join(temp_path, file)
-            modification_time = os.path.getmtime(file_full_path)
-            if(time.time() - modification_time > _MAX_DELTA_CACHE_TIME):
-                logging.debug('removing %s' % file_full_path)
-                os.remove(file_full_path)
-
-class HelperAppLauncherDialog:
-    _com_interfaces_ = interfaces.nsIHelperAppLauncherDialog
-
-    def promptForSaveToFile(self, launcher, window_context,
-                            default_file, suggested_file_extension,
-                            force_prompt=False):
-        file_class = components.classes['@mozilla.org/file/local;1']
-        dest_file = file_class.createInstance(interfaces.nsILocalFile)
-
-        if default_file:
-            default_file = default_file.encode('utf-8', 'replace')
-            base_name, extension = os.path.splitext(default_file)
-        else:
-            base_name = ''
-            if suggested_file_extension:
-                extension = '.' + suggested_file_extension
-            else:
-                extension = ''
-
-        temp_path = os.path.join(activity.get_activity_root(), 'instance')
-        if not os.path.exists(temp_path):
-            os.makedirs(temp_path)
-        fd, file_path = tempfile.mkstemp(dir=temp_path, prefix=base_name,
-                                         suffix=extension)
-        os.close(fd)
-        os.chmod(file_path, 0644)
-        dest_file.initWithPath(file_path)
-
-        interface_id = interfaces.nsIInterfaceRequestor
-        requestor = window_context.queryInterface(interface_id)
-        dom_window = requestor.getInterface(interfaces.nsIDOMWindow)
-        _dest_to_window[file_path] = dom_window
-
-        return dest_file
-
-    def show(self, launcher, context, reason):
-        launcher.saveToDisk(None, False)
-        return NS_OK
-
-
-components.registrar.registerFactory('{64355793-988d-40a5-ba8e-fcde78cac631}',
-                                     'Sugar Download Manager',
-                                     '@mozilla.org/helperapplauncherdialog;1',
-                                     Factory(HelperAppLauncherDialog))
 
+class Download(object):
+    def __init__(self, download, browser):
+        self._download = download
+        self._activity = browser.get_toplevel()
+        self._source = download.get_uri()
 
-class Download:
-    _com_interfaces_ = interfaces.nsITransfer
+        self._download.connect('notify::progress', self.__progress_change_cb)
+        self._download.connect('notify::status', self.__state_change_cb)
+        self._download.connect('error', self.__error_cb)
 
-    def init(self, source, target, display_name, mime_info, start_time,
-             temp_file, cancelable):
-        self._source = source
-        self._mime_type = mime_info.MIMEType
-        self._temp_file = temp_file
-        self._target_file = target.queryInterface(interfaces.nsIFileURL).file
-        self._display_name = display_name
-        self.cancelable = cancelable
         self.datastore_deleted_handler = None
 
         self.dl_jobject = None
@@ -150,43 +73,45 @@ class Download:
         self._last_update_percent = 0
         self._stop_alert = None
 
-        file_path = self._target_file.path.encode('utf-8', 'replace')
-        dom_window = _dest_to_window[file_path]
-        del _dest_to_window[file_path]
+        # figure out download URI
+        temp_path = os.path.join(activity.get_activity_root(), 'instance')
+        if not os.path.exists(temp_path):
+            os.makedirs(temp_path)
 
-        view = hulahop.get_view_for_window(dom_window)
-        logging.debug('Download.init dom_window: %r', dom_window)
-        self._activity = view.get_toplevel()
+        fd, self._dest_path = tempfile.mkstemp(dir=temp_path,
+                                    suffix=download.get_suggested_filename(),
+                                    prefix='tmp')
+        os.close(fd)
+        logging.debug('Download destination path: %s' % self._dest_path)
 
-        return NS_OK
+        self._download.set_destination_uri('file://' + self._dest_path)
+        self._download.start()
 
-    def onStatusChange(self, web_progress, request, status, message):
-        logging.info('Download.onStatusChange(%r, %r, %r, %r)',
-                     web_progress, request, status, message)
+    def __progress_change_cb(self, download, something):
+        progress = self._download.get_progress()
+        self.dl_jobject.metadata['progress'] = str(int(progress * 100))
+        datastore.write(self.dl_jobject)
 
-    def onStateChange(self, web_progress, request, state_flags, status):
-        if state_flags & interfaces.nsIWebProgressListener.STATE_START:
+    def __state_change_cb(self, download, gparamspec):
+        state = self._download.get_status()
+        if state == WebKit.DownloadStatus.STARTED:
             self._create_journal_object()
             self._object_id = self.dl_jobject.object_id
 
             alert = TimeoutAlert(9)
             alert.props.title = _('Download started')
-            alert.props.msg = self._get_file_name()
+            alert.props.msg = _('%s' % self._download.get_suggested_filename())
             self._activity.add_alert(alert)
             alert.connect('response', self.__start_response_cb)
             alert.show()
             global _active_downloads
             _active_downloads.append(self)
 
-        elif state_flags & interfaces.nsIWebProgressListener.STATE_STOP:
-            if NS_FAILED(status):
-                # download cancelled
-                self.cleanup()
-                return
-
+        elif state == WebKit.DownloadStatus.FINISHED:
             self._stop_alert = Alert()
             self._stop_alert.props.title = _('Download completed')
-            self._stop_alert.props.msg = self._get_file_name()
+            self._stop_alert.props.msg = \
+                _('%s' % self._download.get_suggested_filename())
             open_icon = Icon(icon_name='zoom-activity')
             self._stop_alert.add_button(Gtk.ResponseType.APPLY,
                                         _('Show in Journal'), open_icon)
@@ -198,78 +123,48 @@ class Download:
             self._stop_alert.connect('response', self.__stop_response_cb)
             self._stop_alert.show()
 
-            self.dl_jobject.metadata['title'] = self._get_file_name()
+            self.dl_jobject.metadata['title'] = \
+                self._download.get_suggested_filename()
             self.dl_jobject.metadata['description'] = _('From: %s') \
-                % self._source.spec
+                % self._source
             self.dl_jobject.metadata['progress'] = '100'
-            self.dl_jobject.file_path = self._target_file.path
+            self.dl_jobject.file_path = self._dest_path
 
-            if self._mime_type in ['application/octet-stream',
-                                   'application/x-zip']:
-                sniffed_mime_type = mime.get_for_file(self._target_file.path)
-                self.dl_jobject.metadata['mime_type'] = sniffed_mime_type
-
-            if self._check_image_mime_type():
-                self.dl_jobject.metadata['preview'] = self._get_preview_image()
+            # sniff for a mime type, no way to get headers from WebKit
+            sniffed_mime_type = mime.get_for_file(self._dest_path)
+            self.dl_jobject.metadata['mime_type'] = sniffed_mime_type
 
             datastore.write(self.dl_jobject,
                             transfer_ownership=True,
-                            reply_handler=self._internal_save_cb,
-                            error_handler=self._internal_save_error_cb,
-                            timeout=360 * DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
-
-    def _check_image_mime_type(self):
-        for pixbuf_format in GdkPixbuf.Pixbuf.get_formats():
-            if self._mime_type in pixbuf_format['mime_types']:
-                return True
-        return False
+                            reply_handler=self.__internal_save_cb,
+                            error_handler=self.__internal_error_cb,
+                            timeout=360)
 
-    def _get_preview_image(self):
-        preview_width, preview_height = style.zoom(300), style.zoom(225)
-
-        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self._target_file.path)
-        width, height = pixbuf.get_width(), pixbuf.get_height()
-
-        scale = 1
-        if (width > preview_width) or (height > preview_height):
-            scale_x = preview_width / float(width)
-            scale_y = preview_height / float(height)
-            scale = min(scale_x, scale_y)
-
-        pixbuf2 = GdkPixbuf.Pixbuf(GdkPixbuf.Colorspace.RGB, \
-                            pixbuf.get_has_alpha(), \
-                            pixbuf.get_bits_per_sample(), \
-                            preview_width, preview_height)
-        pixbuf2.fill(style.COLOR_WHITE.get_int())
-
-        margin_x = int((preview_width - (width * scale)) / 2)
-        margin_y = int((preview_height - (height * scale)) / 2)
-
-        pixbuf.scale(pixbuf2, margin_x, margin_y, \
-                            preview_width - (margin_x * 2), \
-                            preview_height - (margin_y * 2), \
-                            margin_x, margin_y, scale, scale, \
-                            GdkPixbuf.InterpType.BILINEAR)
+        elif state == WebKit.DownloadStatus.CANCELLED:
+            self.cleanup()
 
-        preview_data = []
+    def __error_cb(self, download, err_code, err_detail, reason):
+        logging.debug('Error downloading URI code %s, detail %s: %s'
+                       % (err_code, err_detail, reason))
 
-        def save_func(buf, data):
-            data.append(buf)
+    def __internal_save_cb(self):
+        logging.debug('Object saved succesfully to the datastore.')
+        self.cleanup()
 
-        pixbuf2.save_to_callback(save_func, 'png', user_data=preview_data)
-        preview_data = ''.join(preview_data)
-        return dbus.ByteArray(preview_data)
+    def __internal_error_cb(self, err):
+        logging.debug('Error saving activity object to datastore: %s' % err)
+        self.cleanup()
 
     def __start_response_cb(self, alert, response_id):
         global _active_downloads
         if response_id is Gtk.ResponseType.CANCEL:
             logging.debug('Download Canceled')
-            logging.debug('target_path=%r', self._target_file.path)
-            self.cancelable.cancel(NS_ERROR_FAILURE)
+            self.cancel()
             try:
                 datastore.delete(self._object_id)
-            except Exception:
-                logging.exception('Object has been deleted already')
+            except Exception, e:
+                logging.warning('Object has been deleted already %s' % e)
+
             self.cleanup()
             if self._stop_alert is not None:
                 self._activity.remove_alert(self._stop_alert)
@@ -292,58 +187,20 @@ class Download:
             self.datastore_deleted_handler.remove()
             self.datastore_deleted_handler = None
 
-        if os.path.isfile(self._target_file.path):
-            os.remove(self._target_file.path)
-        if os.path.isfile(self._target_file.path + '.part'):
-            os.remove(self._target_file.path + '.part')
+        if os.path.isfile(self._dest_path):
+            os.remove(self._dest_path)
 
         if self.dl_jobject is not None:
             self.dl_jobject.destroy()
             self.dl_jobject = None
 
-    def _internal_save_cb(self):
-        self.cleanup()
-
-    def _internal_save_error_cb(self, err):
-        logging.error('Error saving activity object to datastore: %s', err)
-        self.cleanup()
-
-    def onProgressChange64(self, web_progress, request, cur_self_progress,
-                           max_self_progress, cur_total_progress,
-                           max_total_progress):
-        percent = (cur_self_progress * 100) / max_self_progress
-
-        if (time.time() - self._last_update_time) < _MIN_TIME_UPDATE and \
-           (percent - self._last_update_percent) < _MIN_PERCENT_UPDATE:
-            return
-
-        self._last_update_time = time.time()
-        self._last_update_percent = percent
-
-        if percent < 100:
-            self.dl_jobject.metadata['progress'] = str(percent)
-            datastore.write(self.dl_jobject)
-
-    def _get_file_name(self):
-        if self._display_name:
-            return self._display_name
-        elif self._source.scheme == 'data':
-            return 'Data URI'
-        else:
-            uri = self._source
-            if uri == None:
-                return ''
-            cls = components.classes['@mozilla.org/intl/texttosuburi;1']
-            texttosuburi = cls.getService(interfaces.nsITextToSubURI)
-            path = texttosuburi.unEscapeURIForUI(uri.originCharset, uri.spec)
-            location, file_name = os.path.split(path)
-            return file_name
+    def cancel(self):
+        self._download.cancel()
 
     def _create_journal_object(self):
         self.dl_jobject = datastore.create()
-        self.dl_jobject.metadata['title'] = \
-                _('Downloading %(file)s from \n%(source)s.') % \
-                {'file': self._get_file_name(), 'source': self._source.spec}
+        self.dl_jobject.metadata['title'] = _('Downloading %s from \n%s.') % \
+            (self._download.get_suggested_filename(), self._source)
 
         self.dl_jobject.metadata['progress'] = '0'
         self.dl_jobject.metadata['keep'] = '0'
@@ -351,7 +208,7 @@ class Download:
         self.dl_jobject.metadata['preview'] = ''
         self.dl_jobject.metadata['icon-color'] = \
                 profile.get_color().to_string()
-        self.dl_jobject.metadata['mime_type'] = self._mime_type
+        self.dl_jobject.metadata['mime_type'] = ''
         self.dl_jobject.file_path = ''
         datastore.write(self.dl_jobject)
 
@@ -363,109 +220,13 @@ class Download:
             arg0=self.dl_jobject.object_id)
 
     def __datastore_deleted_cb(self, uid):
-        logging.debug('Downloaded entry has been deleted from the data'
-                      ' store: %r', uid)
+        logging.debug('Downloaded entry has been deleted' \
+                          ' from the datastore: %r', uid)
         global _active_downloads
         if self in _active_downloads:
-            # TODO: Use NS_BINDING_ABORTED instead of NS_ERROR_FAILURE.
-            self.cancelable.cancel(NS_ERROR_FAILURE)
+            self.cancel()
             self.cleanup()
 
 
-components.registrar.registerFactory('{23c51569-e9a1-4a92-adeb-3723db82ef7c}',
-                                     'Sugar Download',
-                                     '@mozilla.org/transfer;1',
-                                     Factory(Download))
-
-
-def save_link(url, text, owner_document):
-    # Inspired on Firefox' browser/base/content/nsContextMenu.js:saveLink()
-
-    cls = components.classes["@mozilla.org/network/io-service;1"]
-    io_service = cls.getService(interfaces.nsIIOService)
-    uri = io_service.newURI(url, None, None)
-    channel = io_service.newChannelFromURI(uri)
-
-    auth_prompt_callback = xpcom.server.WrapObject(
-            _AuthPromptCallback(owner_document.defaultView),
-            interfaces.nsIInterfaceRequestor)
-    channel.notificationCallbacks = auth_prompt_callback
-
-    channel.loadFlags = channel.loadFlags | \
-        interfaces.nsIRequest.LOAD_BYPASS_CACHE | \
-        interfaces.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS
-
-    # HACK: when we QI for nsIHttpChannel on objects that implement
-    # just nsIChannel, pyxpcom gets confused trac #1029
-    if uri.scheme == 'http':
-        if _implements_interface(channel, interfaces.nsIHttpChannel):
-            channel.referrer = io_service.newURI(owner_document.documentURI,
-                                                 None, None)
-
-    # kick off the channel with our proxy object as the listener
-    listener = xpcom.server.WrapObject(
-            _SaveLinkProgressListener(owner_document),
-            interfaces.nsIStreamListener)
-    channel.asyncOpen(listener, None)
-
-
-def _implements_interface(obj, interface):
-    try:
-        obj.QueryInterface(interface)
-        return True
-    except xpcom.Exception, e:
-        if e.errno == NS_NOINTERFACE:
-            return False
-        else:
-            raise
-
-
-class _AuthPromptCallback(object):
-    _com_interfaces_ = interfaces.nsIInterfaceRequestor
-
-    def __init__(self, dom_window):
-        self._dom_window = dom_window
-
-    def getInterface(self, uuid):
-        if uuid in [interfaces.nsIAuthPrompt, interfaces.nsIAuthPrompt2]:
-            cls = components.classes["@mozilla.org/embedcomp/window-watcher;1"]
-            window_watcher = cls.getService(interfaces.nsIPromptFactory)
-            return window_watcher.getPrompt(self._dom_window, uuid)
-        return None
-
-
-class _SaveLinkProgressListener(object):
-    _com_interfaces_ = interfaces.nsIStreamListener
-
-    """ an object to proxy the data through to
-    nsIExternalHelperAppService.doContent, which will wait for the appropriate
-    MIME-type headers and then prompt the user with a file picker
-    """
-
-    def __init__(self, owner_document):
-        self._owner_document = owner_document
-        self._external_listener = None
-
-    def onStartRequest(self, request, context):
-        if request.status != NS_OK:
-            logging.error("Error downloading link")
-            return
-
-        class_name = '@mozilla.org/uriloader/external-helper-app-service;1'
-        cls = components.classes[class_name]
-        interface_id = interfaces.nsIExternalHelperAppService
-        external_helper = cls.getService(interface_id)
-
-        channel = request.QueryInterface(interfaces.nsIChannel)
-
-        self._external_listener = \
-            external_helper.doContent(channel.contentType, request,
-                                      self._owner_document.defaultView, True)
-        self._external_listener.onStartRequest(request, context)
-
-    def onStopRequest(self, request, context, statusCode):
-        self._external_listener.onStopRequest(request, context, statusCode)
-
-    def onDataAvailable(self, request, context, inputStream, offset, count):
-        self._external_listener.onDataAvailable(request, context, inputStream,
-                                                offset, count)
+def add_download(download, browser):
+    download = Download(download, browser)
diff --git a/webactivity.py b/webactivity.py
index d3beb8b..7f2eafa 100644
--- a/webactivity.py
+++ b/webactivity.py
@@ -156,8 +156,7 @@ from browser import TabbedView
 from webtoolbar import PrimaryToolbar
 from edittoolbar import EditToolbar
 from viewtoolbar import ViewToolbar
-# FIXME
-# import downloadmanager
+import downloadmanager
 
 # TODO: make the registration clearer SL #3087
 # import filepicker  # pylint: disable=W0611
@@ -574,8 +573,7 @@ class WebActivity(activity.Activity):
     def can_close(self):
         if self._force_close:
             return True
-        # FIXME
-        elif True:  # downloadmanager.can_quit():
+        elif downloadmanager.can_quit():
             return True
         else:
             alert = Alert()
@@ -589,7 +587,8 @@ class WebActivity(activity.Activity):
             cancel_icon = Icon(icon_name='dialog-cancel')
             cancel_label = ngettext('Continue download', 'Continue downloads',
                                     downloadmanager.num_downloads())
-            alert.add_button(Gtk.ResponseType.CANCEL, cancel_label, cancel_icon)
+            alert.add_button(Gtk.ResponseType.CANCEL, cancel_label,
+                             cancel_icon)
             stop_icon = Icon(icon_name='dialog-ok')
             alert.add_button(Gtk.ResponseType.OK, _('Stop'), stop_icon)
             stop_icon.show()
-- 
1.7.7.5



More information about the Sugar-devel mailing list