Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGonzalo Odiard <godiard@gmail.com>2013-03-14 19:22:56 (GMT)
committer Gonzalo Odiard <godiard@gmail.com>2013-03-14 19:22:56 (GMT)
commit660840c80610c6355c96478e8534e74d52ff7469 (patch)
treedb6190ca87c8b386e4510f3200ad3a6bef4b9c74
parent7ccc2db14582e506c8cbdb1852900de527737ce5 (diff)
Add downloadmanager
Stolen from Browse activity.
-rw-r--r--activity.py19
-rw-r--r--downloadmanager.py295
-rw-r--r--server.py14
3 files changed, 324 insertions, 4 deletions
diff --git a/activity.py b/activity.py
index 0884afd..fb128b2 100644
--- a/activity.py
+++ b/activity.py
@@ -16,6 +16,8 @@
import subprocess
+from gi.repository import GObject
+GObject.threads_init()
from gi.repository import Gtk
from gi.repository import WebKit
@@ -24,6 +26,7 @@ from sugar3.activity.widgets import ActivityToolbarButton
from sugar3.activity.widgets import StopButton
from sugar3.graphics.toolbarbox import ToolbarBox
+import downloadmanager
class JournalShare(activity.Activity):
@@ -55,10 +58,26 @@ class JournalShare(activity.Activity):
toolbar_box.show()
self.view = WebKit.WebView()
+ self.view.connect('mime-type-policy-decision-requested',
+ self.__mime_type_policy_cb)
+ self.view.connect('download-requested', self.__download_requested_cb)
+
self.view.load_uri('http://localhost:2500/web/index.html')
self.view.show()
self.set_canvas(self.view)
+ def __mime_type_policy_cb(self, webview, frame, request, mimetype,
+ policy_decision):
+ if not self.view.can_show_mime_type(mimetype):
+ policy_decision.download()
+ return True
+
+ return False
+
+ def __download_requested_cb(self, browser, download):
+ downloadmanager.add_download(download, browser)
+ return True
+
def read_file(self, file_path):
pass
diff --git a/downloadmanager.py b/downloadmanager.py
new file mode 100644
index 0000000..36bcd43
--- /dev/null
+++ b/downloadmanager.py
@@ -0,0 +1,295 @@
+# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2009, Tomeu Vizoso, Lucian Branescu
+#
+# 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
+
+import os
+import logging
+from gettext import gettext as _
+import tempfile
+import dbus
+
+from gi.repository import Gtk
+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.activity import activity
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+_active_downloads = []
+_dest_to_window = {}
+
+SPACE_THRESHOLD = 52428800
+
+def format_float(f):
+ return "%0.2f" % f
+
+def can_quit():
+ return len(_active_downloads) == 0
+
+
+def num_downloads():
+ return len(_active_downloads)
+
+
+def remove_all_downloads():
+ for download in _active_downloads:
+ download.cancel()
+ if download.dl_jobject is not None:
+ datastore.delete(download.dl_jobject.object_id)
+ download.cleanup()
+
+
+class Download(object):
+ def __init__(self, download, browser):
+ self._download = download
+ self._activity = browser.get_toplevel()
+ self._source = download.get_uri()
+
+ self._download.connect('notify::status', self.__state_change_cb)
+ self._download.connect('error', self.__error_cb)
+
+ self.datastore_deleted_handler = None
+
+ self.dl_jobject = None
+ self._object_id = None
+ self._last_update_time = 0
+ self._last_update_percent = 0
+ self._stop_alert = None
+
+ # figure out download URI
+ self.temp_path = os.path.join(activity.get_activity_root(), 'instance')
+ if not os.path.exists(self.temp_path):
+ os.makedirs(self.temp_path)
+
+ fd, self._dest_path = tempfile.mkstemp(dir=self.temp_path,
+ suffix=download.get_suggested_filename(),
+ prefix='tmp')
+ os.close(fd)
+ logging.debug('Download destination path: %s' % self._dest_path)
+
+ # We have to start the download to get 'total-size'
+ # property. It not, 0 is returned
+ self._download.set_destination_uri('file://' + self._dest_path)
+ self._download.start()
+
+ 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 __state_change_cb(self, download, gparamspec):
+ state = self._download.get_status()
+ if state == WebKit.DownloadStatus.STARTED:
+ # Check free space and cancel the download if there is not enough.
+ total_size = self._download.get_total_size()
+ logging.debug('Total size of the file: %s', total_size)
+ enough_space = self.enough_space(
+ total_size, path=self.temp_path)
+ if not enough_space:
+ logging.debug('Download canceled because of Disk Space')
+ self.cancel()
+
+ self._canceled_alert = Alert()
+ self._canceled_alert.props.title = _('Not enough space '
+ 'to download')
+
+ total_size_mb = total_size / 1024.0 ** 2
+ free_space_mb = self._free_available_space(
+ path=self.temp_path) - SPACE_THRESHOLD \
+ / 1024.0 ** 2
+ filename = self._download.get_suggested_filename()
+ self._canceled_alert.props.msg = \
+ _('Download "%{filename}" requires %{total_size_in_mb}' \
+ ' MB of free space, only %{free_space_in_mb} MB' \
+ ' is available' % \
+ {'filename': filename,
+ 'total_size_in_mb': format_float(total_size_mb),
+ 'free_space_in_mb': format_float(free_space_mb)})
+ ok_icon = Icon(icon_name='dialog-ok')
+ self._canceled_alert.add_button(Gtk.ResponseType.OK,
+ _('Ok'), ok_icon)
+ ok_icon.show()
+ self._canceled_alert.connect('response',
+ self.__stop_response_cb)
+ self._activity.add_alert(self._canceled_alert)
+ else:
+ self._download.connect('notify::progress',
+ self.__progress_change_cb)
+ self._create_journal_object()
+ self._object_id = self.dl_jobject.object_id
+
+ alert = TimeoutAlert(9)
+ alert.props.title = _('Download started')
+ 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 == WebKit.DownloadStatus.FINISHED:
+ self._stop_alert = Alert()
+ self._stop_alert.props.title = _('Download completed')
+ 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)
+ open_icon.show()
+ ok_icon = Icon(icon_name='dialog-ok')
+ self._stop_alert.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon)
+ ok_icon.show()
+ self._activity.add_alert(self._stop_alert)
+ self._stop_alert.connect('response', self.__stop_response_cb)
+ self._stop_alert.show()
+
+ self.dl_jobject.metadata['title'] = \
+ self._download.get_suggested_filename()
+ self.dl_jobject.metadata['description'] = _('From: %s') \
+ % self._source
+ self.dl_jobject.metadata['progress'] = '100'
+ self.dl_jobject.file_path = self._dest_path
+
+ # 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_error_cb,
+ timeout=360)
+
+ elif state == WebKit.DownloadStatus.CANCELLED:
+ self.cleanup()
+
+ 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 __internal_save_cb(self):
+ logging.debug('Object saved succesfully to the datastore.')
+ self.cleanup()
+
+ 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')
+ self.cancel()
+ try:
+ datastore.delete(self._object_id)
+ 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)
+
+ self._activity.remove_alert(alert)
+
+ def __stop_response_cb(self, alert, response_id):
+ global _active_downloads
+ if response_id is Gtk.ResponseType.APPLY:
+ logging.debug('Start application with downloaded object')
+ activity.show_object_in_journal(self._object_id)
+ self._activity.remove_alert(alert)
+
+ def cleanup(self):
+ global _active_downloads
+ if self in _active_downloads:
+ _active_downloads.remove(self)
+
+ if self.datastore_deleted_handler is not None:
+ self.datastore_deleted_handler.remove()
+ self.datastore_deleted_handler = None
+
+ 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 cancel(self):
+ self._download.cancel()
+
+ def enough_space(self, size, path='/'):
+ """Check if there is enough (size) free space on path
+
+ size -- free space requested in Kb
+
+ path -- device where the check will be done. For example: '/tmp'
+
+ This method is useful to check the free space, for example,
+ before starting a download from internet, creating a big map
+ in some game or whatever action that needs some space in the
+ Hard Disk.
+ """
+
+ free_space = self._free_available_space(path=path)
+ return free_space - size > SPACE_THRESHOLD
+
+ def _free_available_space(self, path='/'):
+ s = os.statvfs(path)
+ return s.f_bavail * s.f_frsize
+
+ def _create_journal_object(self):
+ self.dl_jobject = datastore.create()
+ self.dl_jobject.metadata['title'] = \
+ _('Downloading %(filename)s from \n%(source)s.') % \
+ {'filename': self._download.get_suggested_filename(),
+ 'source': self._source}
+
+ self.dl_jobject.metadata['progress'] = '0'
+ self.dl_jobject.metadata['keep'] = '0'
+ self.dl_jobject.metadata['buddies'] = ''
+ self.dl_jobject.metadata['preview'] = ''
+ self.dl_jobject.metadata['icon-color'] = \
+ profile.get_color().to_string()
+ self.dl_jobject.metadata['mime_type'] = ''
+ self.dl_jobject.file_path = ''
+ datastore.write(self.dl_jobject)
+
+ bus = dbus.SessionBus()
+ obj = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH)
+ datastore_dbus = dbus.Interface(obj, DS_DBUS_INTERFACE)
+ self.datastore_deleted_handler = datastore_dbus.connect_to_signal(
+ 'Deleted', self.__datastore_deleted_cb,
+ arg0=self.dl_jobject.object_id)
+
+ def __datastore_deleted_cb(self, uid):
+ logging.debug('Downloaded entry has been deleted' \
+ ' from the datastore: %r', uid)
+ global _active_downloads
+ if self in _active_downloads:
+ self.cancel()
+ self.cleanup()
+
+
+def add_download(download, browser):
+ download = Download(download, browser)
diff --git a/server.py b/server.py
index 827f65a..45917bf 100644
--- a/server.py
+++ b/server.py
@@ -68,13 +68,16 @@ class JournalHTTPRequestHandler(network.ChunkedGlibHTTPRequestHandler):
logging.error('Returned datastore/starred')
if self.path.startswith('/datastore/id='):
object_id = self.path[self.path.find('=') + 1:]
- mime_type, content = jm.get_object_by_id(object_id)
- self.send_header_response(mime_type)
+ mime_type, title, content = jm.get_object_by_id(object_id)
+ self.send_header_response(mime_type, title)
self.wfile.write(content)
- def send_header_response(self, mime_type):
+ def send_header_response(self, mime_type, file_name=None):
self.send_response(200)
self.send_header("Content-type", mime_type)
+ if file_name is not None:
+ self.send_header("Content-Disposition",
+ "inline; filename='%s'" % file_name)
self.end_headers()
@@ -102,12 +105,15 @@ class JournalManager():
if mime_type == '':
# TODO: what type should we use if not available?
mime_type = 'application/x-binary'
+ title = None
+ if 'title' in dsobj.metadata:
+ title = dsobj.metadata['title']
f = open(dsobj.file_path, 'r')
# TODO: read all the file in memory?
content = f.read()
f.close()
- return mime_type, content
+ return mime_type, title, content
def get_starred(self):
self.dsobjects, self._nobjects = datastore.find({'keep': '1'})