From df98fd8a1000e93d812d090e42b67c4dbad2e6b9 Mon Sep 17 00:00:00 2001 From: Tomeu Vizoso Date: Mon, 15 Dec 2008 16:34:31 +0000 Subject: First go at adding file transfer to the journal --- diff --git a/bin/sugar-session b/bin/sugar-session index 7118c21..fc10eb8 100644 --- a/bin/sugar-session +++ b/bin/sugar-session @@ -120,6 +120,10 @@ def setup_notification_service_cb(): from jarabe.model import notifications notifications.init() +def setup_file_transfer_cb(): + from jarabe.model import filetransfer + filetransfer.init() + def main(): cleanup_logs() logger.start('shell') @@ -155,6 +159,7 @@ def main(): gobject.idle_add(setup_keyhandler_cb) gobject.idle_add(setup_journal_cb) gobject.idle_add(setup_notification_service_cb) + gobject.idle_add(setup_file_transfer_cb) gobject.idle_add(show_software_updates_cb, home_window) try: diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py index 8821236..1ff044a 100644 --- a/src/jarabe/frame/activitiestray.py +++ b/src/jarabe/frame/activitiestray.py @@ -17,8 +17,11 @@ import logging from gettext import gettext as _ -import gconf +import tempfile +import os +import gconf +import dbus import gtk from sugar.graphics import style @@ -31,17 +34,23 @@ from sugar.graphics.palette import Palette, WidgetInvoker from sugar.graphics.menuitem import MenuItem from sugar.activity.activityhandle import ActivityHandle from sugar.activity import activityfactory +from sugar import mime from jarabe.model import shell from jarabe.model import neighborhood from jarabe.model import owner from jarabe.model import bundleregistry +from jarabe.model import filetransfer from jarabe.view.palettes import JournalPalette, CurrentActivityPalette from jarabe.view.pulsingicon import PulsingIcon from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.frame.notification import NotificationIcon import jarabe.frame +DS_DBUS_SERVICE = "org.laptop.sugar.DataStore" +DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore" +DS_DBUS_PATH = "/org/laptop/sugar/DataStore" + class ActivityButton(RadioToolButton): def __init__(self, home_activity, group): RadioToolButton.__init__(self, group=group) @@ -83,12 +92,15 @@ class ActivityButton(RadioToolButton): self._icon.props.pulsing = False home_activity.disconnect(self._notify_launching_hid) - class BaseInviteButton(ToolButton): def __init__(self, invite): ToolButton.__init__(self) self._invite = invite + self._icon = Icon() + self.set_icon_widget(self._icon) + self._icon.show() + self.connect('clicked', self.__clicked_cb) self.connect('destroy', self.__destroy_cb) self._notif_icon = NotificationIcon() @@ -127,8 +139,6 @@ class ActivityInviteButton(BaseInviteButton): self._icon.props.file = activity_model.get_icon_name() else: self._icon.props.icon_name = 'image-missing' - self.set_icon_widget(self._icon) - self._icon.show() palette = ActivityInvitePalette(invite) palette.props.invoker = FrameWidgetInvoker(self) @@ -177,8 +187,6 @@ class PrivateInviteButton(BaseInviteButton): self._icon.props.file = self._bundle.get_icon() else: self._icon.props.icon_name = 'image-missing' - self.set_icon_widget(self._icon) - self._icon.show() palette = PrivateInvitePalette(invite) palette.props.invoker = FrameWidgetInvoker(self) @@ -310,6 +318,8 @@ class ActivitiesTray(HTray): self._invites.connect('invite-added', self.__invite_added_cb) self._invites.connect('invite-removed', self.__invite_removed_cb) + filetransfer.new_file_transfer.connect(self.__new_file_transfer_cb) + def __activity_added_cb(self, home_model, home_activity): logging.debug('__activity_added_cb: %r' % home_activity) if self.get_children(): @@ -370,7 +380,7 @@ class ActivitiesTray(HTray): self._invites.remove_invite(invite) else: self._invites.remove_private_invite(invite) - + def __invite_added_cb(self, invites, invite): self._add_invite(invite) @@ -398,3 +408,403 @@ class ActivitiesTray(HTray): self._invite_to_item[invite].destroy() del self._invite_to_item[invite] + def __new_file_transfer_cb(self, **kwargs): + file_transfer = kwargs['file_transfer'] + logging.debug('__new_file_transfer_cb %r' % file_transfer) + + if isinstance(file_transfer, filetransfer.IncomingFileTransfer): + button = IncomingTransferButton(file_transfer) + elif isinstance(file_transfer, filetransfer.OutgoingFileTransfer): + button = OutgoingTransferButton(file_transfer) + + self.add_item(button) + button.show() + +class BaseTransferButton(ToolButton): + """Button with a notification attached + """ + def __init__(self): + ToolButton.__init__(self) + icon = Icon() + self.props.icon_widget = icon + icon.show() + + self.notif_icon = NotificationIcon() + self.notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + def __button_release_event_cb(self, icon, event): + if self.notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self.notif_icon) + self.notif_icon = None + +class IncomingTransferButton(BaseTransferButton): + """UI element representing an ongoing incoming file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self) + + self._object_id = None + self._metadata = {} + self._file_transfer = file_transfer + self._file_transfer.connect('notify::state', self.__notify_state_cb) + self._file_transfer.connect('notify::transferred-bytes', + self.__notify_transferred_bytes_cb) + + icon_name = mime.get_mime_icon(file_transfer.mime_type) + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if not info: + # display standard icon when icon for mime type is not found + icon_name = 'application-octet-stream' + + icon_color = XoColor(file_transfer.buddy.props.color) + + self.props.icon_widget.props.icon_name = icon_name + self.props.icon_widget.props.xo_color = icon_color + + self.notif_icon.props.icon_name = icon_name + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = IncomingTransferPalette(self._file_transfer) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + + def __notify_state_cb(self, file_transfer, pspec): + if file_transfer.props.state == filetransfer.FT_STATE_OPEN: + logging.debug('__notify_state_cb OPEN') + self._metadata['title'] = file_transfer.title + self._metadata['description'] = file_transfer.description + self._metadata['progress'] = '0' + self._metadata['keep'] = '0' + self._metadata['buddies'] = '' + self._metadata['preview'] = '' + self._metadata['icon-color'] = file_transfer.buddy.props.color + self._metadata['mime_type'] = file_transfer.mime_type + + datastore = self._get_datastore() + file_path = '' + transfer_ownership = True + self._object_id = datastore.create(self._metadata, file_path, + transfer_ownership) + + elif file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + logging.debug('__notify_state_cb COMPLETED') + self._metadata['progress'] = '100' + + datastore = self._get_datastore() + file_path = file_transfer.destination_path + transfer_ownership = True + datastore.update(self._object_id, self._metadata, file_path, + transfer_ownership, + reply_handler=self.__reply_handler_cb, + error_handler=self.__error_handler_cb) + + elif file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + logging.debug('__notify_state_cb CANCELLED') + if self._object_id is not None: + datastore.delete(self._object_id, + reply_handler=self.__reply_handler_cb, + error_handler=self.__error_handler_cb) + self._object_id = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + progress = file_transfer.props.transferred_bytes / \ + file_transfer.file_size + self._metadata['progress'] = str(progress * 100) + + datastore = self._get_datastore() + file_path = '' + transfer_ownership = True + datastore.update(self._object_id, self._metadata, file_path, + transfer_ownership, + reply_handler=self.__reply_handler_cb, + error_handler=self.__error_handler_cb) + + def _get_datastore(self): + bus = dbus.SessionBus() + remote_object = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH) + return dbus.Interface(remote_object, DS_DBUS_INTERFACE) + + def __reply_handler_cb(self): + logging.debug('__reply_handler_cb %r' % self._object_id) + + def __error_handler_cb(self, error): + logging.debug('__error_handler_cb %r %s' % (self._object_id, error)) + +class OutgoingTransferButton(BaseTransferButton): + """UI element representing an ongoing outgoing file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self) + + self._file_transfer = file_transfer + + icon_name = mime.get_mime_icon(file_transfer.mime_type) + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if not info: + # display standard icon when icon for mime type is not found + icon_name = 'application-octet-stream' + + client = gconf.client_get_default() + icon_color = XoColor(client.get_string("/desktop/sugar/user/color")) + + self.props.icon_widget.props.icon_name = icon_name + self.props.icon_widget.props.xo_color = icon_color + + self.notif_icon.props.icon_name = icon_name + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = OutgoingTransferPalette(self._file_transfer) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + +class BaseTransferPalette(Palette): + """Base palette class for frame or notification icon for file transfers + """ + def __init__(self, file_transfer): + Palette.__init__(self, file_transfer.title) + + self.file_transfer = file_transfer + + self.progress_bar = None + self.progress_label = None + self._notify_transferred_bytes_handler = None + + self.connect('popup', self.__popup_cb) + self.connect('popdown', self.__popdown_cb) + + def __popup_cb(self, palette): + self.update_progress() + self._notify_transferred_bytes_handler = \ + self.file_transfer.connect('notify::transferred_bytes', + self.__notify_transferred_bytes_cb) + + def __popdown_cb(self, palette): + if self._notify_transferred_bytes_handler is not None: + self.file_transfer.disconnect( + self._notify_transferred_bytes_handler) + self._notify_transferred_bytes_handler = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + self.update_progress() + + def _format_size(self, size): + if size < 1024: + return _('%dB') % size + elif size < 1048576: + return _('%dKB') % (size / 1024) + else: + return _('%dMB') % (size / 1048576) + + def update_progress(self): + logging.debug('update_progress: %r' % self.file_transfer.props.transferred_bytes) + if self.progress_bar is None: + return + + self.progress_bar.props.fraction = \ + self.file_transfer.props.transferred_bytes / \ + float(self.file_transfer.file_size) + logging.debug('update_progress: %r' % self.progress_bar.props.fraction) + + transferred = self._format_size( + self.file_transfer.props.transferred_bytes) + total = self._format_size(self.file_transfer.file_size) + self.progress_label.props.label = _('%s of %s') % (transferred, total) + +class IncomingTransferPalette(BaseTransferPalette): + """Palette for frame or notification icon for incoming file transfers + """ + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = self.file_transfer.buddy.props.nick + self.props.secondary_text = _('Transfer from %r') % nick + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + logging.debug('_update state: %r' % self.file_transfer.props.state) + if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: + menu_item = MenuItem(_('Accept'), icon_name='dialog-ok') + menu_item.connect('activate', self.__accept_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label(_('%s (%s)') % (size, type_description)) + vbox.add(label) + label.show() + + elif self.file_transfer.props.state in \ + [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + # TODO: What to do here? + self.update_progress() + elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + # TODO: What to do here? + self.update_progress() + + def __accept_activate_cb(self, menu_item): + #TODO: figure out the best place to get rid of that temp file + extension = mime.get_primary_extension(self.file_transfer.mime_type) + fd, file_path = tempfile.mkstemp(suffix=extension, + prefix=self._sanitize(self.file_transfer.title)) + os.close(fd) + os.unlink(file_path) + + self.file_transfer.accept(file_path) + + def _sanitize(self, file_name): + file_name = file_name.replace('/', '_') + file_name = file_name.replace('.', '_') + file_name = file_name.replace('?', '_') + return file_name + + def __decline_activate_cb(self, menu_item): + self.file_transfer.decline() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + +class OutgoingTransferPalette(Palette): + """Palette for frame or notification icon for outgoing file transfers + """ + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = file_transfer.buddy.props.nick + self.props.secondary_text = _('Transfer to %r') % nick + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + logging.debug('_update state: %r' % self.file_transfer.props.state) + if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label(_('%s (%s)') % (size, type_description)) + vbox.add(label) + label.show() + + elif self.file_transfer.props.state in \ + [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + # TODO: What to do here? + self.update_progress() + elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + # TODO: What to do here? + self.update_progress() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index a9e3f85..6832750 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -16,7 +16,7 @@ from gettext import gettext as _ import logging - + import gobject import gtk import gconf @@ -26,9 +26,11 @@ from sugar.graphics.palette import Palette from sugar.graphics.menuitem import MenuItem from sugar.graphics.icon import Icon from sugar.graphics.xocolor import XoColor +from sugar import mime from jarabe.model import bundleregistry from jarabe.model import friends +from jarabe.model import filetransfer from jarabe.journal import misc from jarabe.journal import model @@ -78,11 +80,13 @@ class ObjectPalette(Palette): menu_item.show() menu_item = MenuItem(_('Send to'), 'list-remove') - #menu_item.connect('activate', self.__sendto_activate_cb) self.menu.append(menu_item) - menu_item.set_submenu(FriendsMenu()) menu_item.show() + friends_menu = FriendsMenu() + friends_menu.connect('friend-selected', self.__friend_selected_cb) + menu_item.set_submenu(friends_menu) + menu_item = MenuItem(_('Erase'), 'list-remove') menu_item.connect('activate', self.__erase_activate_cb) self.menu.append(menu_item) @@ -114,6 +118,21 @@ class ObjectPalette(Palette): registry.uninstall(bundle) model.delete(self._metadata['uid']) + def __friend_selected_cb(self, menu_item, buddy): + logging.debug('__friend_selected_cb') + #TODO: figure out the best place to get rid of that temp file + file_name = model.get_file(self._metadata['uid']) + + title = str(self._metadata['title']) + description = str(self._metadata.get('description', '')) + mime_type = str(self._metadata['mime_type']) + + if not mime_type: + mime_type = mime.get_for_file(file_name) + + filetransfer.start_transfer(buddy, file_name, title, description, + mime_type) + class FriendsMenu(gtk.Menu): __gtype_name__ = 'JournalFriendsMenu' diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am index 91d9a3e..399db65 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -3,6 +3,7 @@ sugar_PYTHON = \ __init__.py \ buddy.py \ bundleregistry.py \ + filetransfer.py \ friends.py \ invites.py \ owner.py \ diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py new file mode 100644 index 0000000..a44af75 --- /dev/null +++ b/src/jarabe/model/filetransfer.py @@ -0,0 +1,331 @@ +# Copyright (C) 2008 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 +# 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 +import socket + +import gobject +import gio +import dbus +from telepathy.interfaces import CONNECTION_INTERFACE_REQUESTS, CHANNEL +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT, \ + SOCKET_ADDRESS_TYPE_UNIX, \ + SOCKET_ACCESS_CONTROL_LOCALHOST +from telepathy.client import Connection, Channel + +from sugar.presence import presenceservice +from sugar import dispatch + +from jarabe.util.telepathy import connection_watcher + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +# FIXME: use constants from tp-python once the spec is undrafted +CHANNEL_TYPE_FILE_TRANSFER = \ + 'org.freedesktop.Telepathy.Channel.Type.FileTransfer.DRAFT' + +class StreamSplicer(gobject.GObject): + _CHUNK_SIZE = 102400 # 100K + __gsignals__ = { + 'finished': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + def __init__(self, input_stream, output_stream): + gobject.GObject.__init__(self) + + self._input_stream = input_stream + self._output_stream = output_stream + self._pending_buffers = [] + + def start(self): + 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) + #logging.debug('__read_async_cb %r' % len(data)) + if data: + self._pending_buffers.append(data) + if len(data) == self._CHUNK_SIZE: + self._input_stream.read_async(self._CHUNK_SIZE, + self.__read_async_cb, + gobject.PRIORITY_LOW) + + if not data or len(data) < self._CHUNK_SIZE: + logging.debug('closing input stream') + self._input_stream.close() + + self._write_next_buffer() + + def __write_async_cb(self, output_stream, result, user_data): + count_ = output_stream.write_finish(result) + + if not self._pending_buffers and \ + not self._output_stream.has_pending() and \ + not self._input_stream.has_pending(): + logging.debug('closing output stream') + output_stream.close() + self.emit('finished') + else: + 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) + +class BaseFileTransfer(gobject.GObject): + + def __init__(self, connection): + gobject.GObject.__init__(self) + self._connection = connection + self._state = FT_STATE_NONE + self._transferred_bytes = 0 + + self.channel = None + self.buddy = None + self.title = None + self.file_size = None + self.description = None + self.mime_type = None + self.initial_offset = 0 + + def set_channel(self, channel): + self.channel = channel + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'FileTransferStateChanged', self.__state_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'TransferredBytesChanged', self.__transferred_bytes_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'InitialOffsetDefined', self.__initial_offset_defined_cb) + + channel_properties = self.channel[dbus.PROPERTIES_IFACE] + + props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER) + self._state = props['State'] + self.title = props['Filename'] + self.file_size = props['Size'] + self.description = props['Description'] + self.mime_type = props['ContentType'] + + handle = channel_properties.Get(CHANNEL, 'TargetHandle') + presence_service = presenceservice.get_instance() + self.buddy = presence_service.get_buddy_by_telepathy_handle( + self._connection.service_name, + self._connection.object_path, + handle) + + def __transferred_bytes_changed_cb(self, transferred_bytes): + logging.debug('__transferred_bytes_changed_cb %r' % transferred_bytes) + self.props.transferred_bytes = transferred_bytes + + def _set_transferred_bytes(self, transferred_bytes): + self._transferred_bytes = transferred_bytes + + def _get_transferred_bytes(self): + return self._transferred_bytes + + transferred_bytes = gobject.property(type=int, default=0, + getter=_get_transferred_bytes, setter=_set_transferred_bytes) + + def __initial_offset_defined_cb(self, offset): + logging.debug('__initial_offset_defined_cb %r' % offset) + self.initial_offset = offset + + def __state_changed_cb(self, state, reason): + logging.debug('__state_changed_cb %r %r' % (state, reason)) + self.props.state = state + + def _set_state(self, state): + self._state = state + + def _get_state(self): + return self._state + + state = gobject.property(type=int, getter=_get_state, setter=_set_state) + + def cancel(self): + self.channel[CHANNEL].Close() + +class IncomingFileTransfer(BaseFileTransfer): + def __init__(self, connection, object_path, props): + BaseFileTransfer.__init__(self, connection) + + channel = Channel(connection.service_name, object_path) + self.set_channel(channel) + + self.connect('notify::state', self.__notify_state_cb) + + self.destination_path = None + self._socket_address = None + self._socket = None + self._splicer = None + + def accept(self, destination_path): + if os.path.exists(destination_path): + raise ValueError('Destination path already exists: %r' % \ + destination_path) + + self.destination_path = destination_path + + channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_ft.AcceptFile(SOCKET_ADDRESS_TYPE_UNIX, + SOCKET_ACCESS_CONTROL_LOCALHOST, '', 0) + + def decline(self): + self.channel[CHANNEL].Close() + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r' % self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # close the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + input_stream = gio.unix.InputStream(self._socket.fileno(), True) + + destination_file = gio.File(self.destination_path) + if self.initial_offset == 0: + output_stream = destination_file.create() + else: + output_stream = destination_file.append_to() + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + +class OutgoingFileTransfer(BaseFileTransfer): + def __init__(self, buddy, file_name, title, description, mime_type): + + presence_service = presenceservice.get_instance() + name, path = presence_service.get_preferred_connection() + connection = Connection(name, path, + ready_handler=self.__connection_ready_cb) + + BaseFileTransfer.__init__(self, connection) + self.connect('notify::state', self.__notify_state_cb) + + self._file_name = file_name + self._socket_address = None + self._socket = None + self._splicer = None + self._output_stream = None + + self.buddy = buddy.get_buddy() + self.title = title + self.file_size = os.stat(file_name).st_size + self.description = description + self.mime_type = mime_type + + def __connection_ready_cb(self, connection): + handle = self._get_buddy_handle() + + requests = connection[CONNECTION_INTERFACE_REQUESTS] + object_path, properties_ = requests.CreateChannel({ + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + CHANNEL + '.TargetHandle': handle, + CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self.mime_type, + CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self.title, + CHANNEL_TYPE_FILE_TRANSFER + '.Size': self.file_size, + CHANNEL_TYPE_FILE_TRANSFER + '.Description': self.description, + CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0}) + + self.set_channel(Channel(connection.service_name, object_path)) + + channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_file_transfer.ProvideFile( + SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '') + + def _get_buddy_handle(self): + object_path = self.buddy.object_path() + + bus = dbus.SessionBus() + remote_object = bus.get_object('org.laptop.Sugar.Presence', object_path) + ps_buddy = dbus.Interface(remote_object, + 'org.laptop.Sugar.Presence.Buddy') + + handles = ps_buddy.GetTelepathyHandles() + logging.debug('_get_buddy_handle %r' % handles) + + bus_name, object_path, handle = handles[0] + + return handle + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r' % self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # closes the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + output_stream = gio.unix.OutputStream(self._socket.fileno(), True) + + logging.debug('opening %s for reading' % self._file_name) + input_stream = gio.File(self._file_name).read() + if self.initial_offset > 0: + input_stream.skip(self.initial_offset) + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + + def cancel(self): + self.channel[CHANNEL].Close() + +def _new_channels_cb(connection, channels): + for object_path, props in channels: + if props[CHANNEL + '.ChannelType'] == CHANNEL_TYPE_FILE_TRANSFER and \ + not props[CHANNEL + '.Requested']: + + logging.debug('__new_channels_cb %r' % object_path) + + incoming_file_transfer = IncomingFileTransfer(connection, + object_path, props) + new_file_transfer.send(None, file_transfer=incoming_file_transfer) + +def _monitor_connection(connection): + connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal('NewChannels', + lambda channels: _new_channels_cb(connection, channels)) + +def _connection_addded_cb(conn_watcher, connection): + _monitor_connection(connection) + +def init(): + conn_watcher = connection_watcher.ConnectionWatcher() + conn_watcher.connect('connection-added', _connection_addded_cb) + + for connection in conn_watcher.get_connections(): + _monitor_connection(connection) + +def start_transfer(buddy, file_name, title, description, mime_type): + outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title, + description, mime_type) + new_file_transfer.send(None, file_transfer=outgoing_file_transfer) + +new_file_transfer = dispatch.Signal() + -- cgit v0.9.1