[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