Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bin/sugar-session5
-rw-r--r--src/jarabe/frame/activitiestray.py424
-rw-r--r--src/jarabe/journal/palettes.py25
-rw-r--r--src/jarabe/model/Makefile.am1
-rw-r--r--src/jarabe/model/filetransfer.py331
5 files changed, 776 insertions, 10 deletions
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()
+