From 29a4f0282209c06a2c998857dae6f2f3bf1152b1 Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Sat, 9 May 2020 00:46:43 +0100 Subject: [PATCH 1/8] Port to Python 3 * Run 2to3 * Port telepathy static bindings to TelepathyGLib Signed-off-by: Ibiam Chihurumnaya --- activity/activity.info | 2 +- viewslides.py | 115 ++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/activity/activity.info b/activity/activity.info index abd40a1..77f58ab 100644 --- a/activity/activity.info +++ b/activity/activity.info @@ -1,7 +1,7 @@ [Activity] name = View Slides bundle_id = org.laptop.ViewSlidesActivity -exec = sugar-activity viewslides.ViewSlidesActivity +exec = sugar-activity3 viewslides.ViewSlidesActivity activity_version = 14 icon = ViewSlides show_launcher = yes diff --git a/viewslides.py b/viewslides.py index ce29f89..bd9598b 100644 --- a/viewslides.py +++ b/viewslides.py @@ -47,8 +47,8 @@ import dbus from gi.repository import GLib from gi.repository import GObject -import telepathy -import cPickle as pickle +from gi.repository import TelepathyGLib +import pickle from decimal import * import xopower @@ -124,9 +124,8 @@ def add_bookmark(self, page): def remove_bookmark(self, page): try: self.bookmarks.remove(page) - # print 'bookmarks=', self.bookmarks except ValueError: - print 'page already not bookmarked', page + print('page already not bookmarked', page) def get_bookmarks(self): self.bookmarks.sort() @@ -321,8 +320,7 @@ def __init__(self, handle): self.pickle_file_temp = os.path.join( self.get_activity_root(), 'instance', - 'pkl%i' % - time.time()) + 'pkl{}'.format(time.time())) self.annotations = Annotations(self.pickle_file_temp) xopower.setup_idle_timeout() @@ -365,8 +363,7 @@ def __init__(self, handle): self.tempfile = os.path.join( self.get_activity_root(), 'instance', - 'tmp%i' % - time.time()) + 'tmp{}'.format(time.time())) self.show_image_tables(True) @@ -585,9 +582,9 @@ def update_bookmark_button(self, state): def load_journal_table(self): ds_objects, num_objects = datastore.find({'mime_type': ['image/jpeg', 'image/gif', - 'image/tiff', 'image/png']}, properties=['uid', 'title', 'mime_type']) + 'image/tiff', 'image/png']}, properties=['uid', 'title', 'mime_type']) self.ls_right.clear() - for i in xrange(0, num_objects, 1): + for i in range(0, num_objects, 1): iter = self.ls_right.append() title = ds_objects[i].metadata['title'] mime_type = ds_objects[i].metadata['mime_type'] @@ -711,7 +708,7 @@ def add_image(self): # Assign a file path to create if one doesn't exist yet if self.tempfile is None: self.tempfile = os.path.join(self.get_activity_root(), 'instance', - 'tmp%i' % time.time()) + 'tmp{}'.format(time.time())) try: if os.path.exists(self.tempfile): zf = zipfile.ZipFile(self.tempfile, 'a') @@ -728,7 +725,7 @@ def add_image(self): arcname) self._slides_toolbar._add_image.props.sensitive = False except BadZipfile as err: - print 'Error opening the zip file: %s' % (err) + print('Error opening the zip file: {}'.format(err)) self._alert('Error', 'Error opening the zip file') def remove_image(self): @@ -784,8 +781,8 @@ def rewrite_zip(self): if not self.is_dirty: return new_zipfile = os.path.join(self.get_activity_root(), 'instance', - 'rewrite%i' % time.time()) - print self.tempfile, new_zipfile + 'rewrite{}'.format(time.time())) + print(self.tempfile, new_zipfile) zf_new = zipfile.ZipFile(new_zipfile, 'w') zf_old = zipfile.ZipFile(self.tempfile, 'r') for row in self.ls_left: @@ -796,7 +793,7 @@ def rewrite_zip(self): fname = os.path.join( self.get_activity_root(), 'instance', outfn) zf_new.write(fname.encode("utf-8"), new_file.encode("utf-8")) - print 'rewriting', new_file + print('rewriting', new_file) os.remove(fname) zf_old.close() zf_new.close() @@ -809,8 +806,8 @@ def final_rewrite_zip(self): return new_zipfile = os.path.join(self.get_activity_root(), 'instance', - 'rewrite%i' % time.time()) - print self.tempfile, new_zipfile + 'rewrite{}'.format(time.time())) + print(self.tempfile, new_zipfile) zf_new = zipfile.ZipFile(new_zipfile, 'w') zf_old = zipfile.ZipFile(self.tempfile, 'r') image_files = zf_old.namelist() @@ -1115,7 +1112,7 @@ def save_extracted_file(self, zipfile, filename): try: filebytes = zipfile.read(filename) except BadZipfile as err: - print 'Error opening the zip file: %s' % (err) + print('Error opening the zip file: {}'.format(err)) return False except KeyError as err: self._alert('Key Error', 'Zipfile key not found: ' + str(filename)) @@ -1150,8 +1147,7 @@ def read_file(self, file_path): tempfile = os.path.join( self.get_activity_root(), 'instance', - 'tmp%i' % - time.time()) + 'tmp{}'.format(time.time())) os.link(file_path, tempfile) self.tempfile = tempfile self.get_saved_page_number() @@ -1240,7 +1236,7 @@ def _load_document(self, file_path): self.watch_for_tubes() self._share_document() else: - print 'Not a zipfile', file_path + print('Not a zipfile', file_path) self.tempfile = None def write_file(self, file_path): @@ -1248,7 +1244,7 @@ def write_file(self, file_path): # Assign a file path to create if one doesn't exist yet if self.tempfile is None: self.tempfile = os.path.join(self.get_activity_root(), 'instance', - 'tmp%i' % time.time()) + 'tmp{}'.format(time.time())) if not os.path.exists(self.tempfile): zf = zipfile.ZipFile(self.tempfile, 'w') zf.writestr("filler.txt", "filler") @@ -1273,8 +1269,7 @@ def write_file(self, file_path): self.final_rewrite_zip() os.link(self.tempfile, file_path) _logger.debug( - "Removing temp file %s because we will close", - self.tempfile) + "Removing temp file {} because we will close".format(self.tempfile)) os.unlink(self.tempfile) os.remove(self.pickle_file_temp) self.tempfile = None @@ -1302,26 +1297,26 @@ def _download_result_cb(self, getter, tempfile, suggested_name, tube_id): self.tempfile = tempfile file_path = os.path.join(self.get_activity_root(), 'instance', - '%i' % time.time()) - _logger.debug("Saving file %s to datastore...", file_path) + '{}'.format(time.time())) + _logger.debug("Saving file {} to datastore...".format(file_path)) os.link(tempfile, file_path) self._jobject.file_path = file_path datastore.write(self._jobject, transfer_ownership=True) - _logger.debug("Got document %s (%s) from tube %u", - tempfile, suggested_name, tube_id) + _logger.debug("Got document {} ({}) from tube {}".format( + tempfile, suggested_name, tube_id)) self._load_document(tempfile) self.save() self.progressbar.hide() def _download_progress_cb(self, getter, bytes_downloaded, tube_id): if self._download_content_length > 0: - _logger.debug("Downloaded %u of %u bytes from tube %u...", + _logger.debug("Downloaded {} of {} bytes from tube {}...".format( bytes_downloaded, self._download_content_length, - tube_id) + tube_id)) else: - _logger.debug("Downloaded %u bytes from tube %u...", - bytes_downloaded, tube_id) + _logger.debug("Downloaded {} bytes from tube {}...".format( + bytes_downloaded, tube_id)) total = self._download_content_length self.set_downloaded_bytes(bytes_downloaded, total) Gdk.threads_enter() @@ -1331,8 +1326,8 @@ def _download_progress_cb(self, getter, bytes_downloaded, tube_id): def _download_error_cb(self, getter, err, tube_id): self.progressbar.hide() - _logger.debug("Error getting document from tube %u: %s", - tube_id, err) + _logger.debug("Error getting document from tube {}: {}".format( + tube_id, err)) self._alert('Failure', 'Error getting document from tube') self._want_document = True self._download_content_length = 0 @@ -1342,29 +1337,29 @@ def _download_error_cb(self, getter, err, tube_id): def _download_document(self, tube_id, path): # FIXME: should ideally have the CM listen on a Unix socket # instead of IPv4 (might be more compatible with Rainbow) - chan = self._shared_activity.telepathy_tubes_chan - iface = chan[telepathy.CHANNEL_TYPE_TUBES] + chan = self.shared_activity.telepathy_tubes_chan + iface = chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES] addr = iface.AcceptStreamTube( tube_id, - telepathy.SOCKET_ADDRESS_TYPE_IPV4, - telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, + TelepathyGLib.SocketAddressType.IPV4, + TelepathyGLib.SocketAccessControl.LOCALHOST, 0, utf8_strings=True) - _logger.debug('Accepted stream tube: listening address is %r', addr) + _logger.debug('Accepted stream tube: listening address is {}'.format(addr)) # SOCKET_ADDRESS_TYPE_IPV4 is defined to have addresses of type '(sq)' assert isinstance(addr, dbus.Struct) assert len(addr) == 2 assert isinstance(addr[0], str) - assert isinstance(addr[1], (int, long)) + assert isinstance(addr[1], int) assert addr[1] > 0 and addr[1] < 65536 port = int(addr[1]) - getter = ReadURLDownloader("http://%s:%d/document" - % (addr[0], port)) + getter = ReadURLDownloader("http://{}:{}/document".format( + addr[0], port)) getter.connect("finished", self._download_result_cb, tube_id) getter.connect("progress", self._download_progress_cb, tube_id) getter.connect("error", self._download_error_cb, tube_id) - _logger.debug("Starting download to %s...", path) + _logger.debug("Starting download to {}...".format(path)) getter.start(path) self._download_content_length = getter.get_content_length() self._download_content_type = getter.get_content_type() @@ -1377,16 +1372,16 @@ def _get_document(self): # Assign a file path to download if one doesn't exist yet if not self._jobject.file_path: path = os.path.join(self.get_activity_root(), 'instance', - 'tmp%i' % time.time()) + 'tmp{}'.format(time.time())) else: path = self._jobject.file_path # Pick an arbitrary tube we can try to download the document from try: tube_id = self.unused_download_tubes.pop() - except (ValueError, KeyError) as e: - _logger.debug('No tubes to get the document from right now: %s', - e) + except (ValueError, KeyError) as Error: + _logger.debug('No tubes to get the document from right now: {}'.format( + Error)) return False # Avoid trying to download the document multiple times at once @@ -1408,39 +1403,39 @@ def _share_document(self): # FIXME: should ideally have the fileserver listen on a Unix socket # instead of IPv4 (might be more compatible with Rainbow) - _logger.debug('Starting HTTP server on port %d', self.port) + _logger.debug('Starting HTTP server on port {}'.format(self.port)) self._fileserver = ReadHTTPServer(("", self.port), self.tempfile) # Make a tube for it - chan = self._shared_activity.telepathy_tubes_chan - iface = chan[telepathy.CHANNEL_TYPE_TUBES] + chan = self.shared_activity.telepathy_tubes_chan + iface = chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES] self._fileserver_tube_id = iface.OfferStreamTube( READ_STREAM_SERVICE, {}, - telepathy.SOCKET_ADDRESS_TYPE_IPV4, + TelepathyGLib.SocketAddressType.IPV4, ('127.0.0.1', dbus.UInt16( self.port)), - telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, + TelepathyGLib.SocketAccessControl.LOCALHOST, 0) def watch_for_tubes(self): """Watch for new tubes.""" - tubes_chan = self._shared_activity.telepathy_tubes_chan + tubes_chan = self.shared_activity.telepathy_tubes_chan - tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( + tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].connect_to_signal( 'NewTube', self._new_tube_cb) - tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].ListTubes( reply_handler=self._list_tubes_reply_cb, error_handler=self._list_tubes_error_cb) def _new_tube_cb(self, tube_id, initiator, tube_type, service, params, state): """Callback when a new tube becomes available.""" - _logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' - 'params=%r state=%d', tube_id, initiator, tube_type, - service, params, state) + _logger.debug('New tube: ID={} initator={} type={} service={} ' + 'params={} state={}'.format(tube_id, initiator, tube_type, + service, params, state)) if service == READ_STREAM_SERVICE: _logger.debug('I could download from that tube') self.unused_download_tubes.add(tube_id) @@ -1453,9 +1448,9 @@ def _list_tubes_reply_cb(self, tubes): for tube_info in tubes: self._new_tube_cb(*tube_info) - def _list_tubes_error_cb(self, e): + def _list_tubes_error_cb(self, error): """Handle ListTubes error by logging.""" - _logger.error('ListTubes() failed: %s', e) + _logger.error('ListTubes() failed: {}'.format(error)) def _shared_cb(self, activityid): """Callback when activity shared. From 9fdf1da042223c9e002a56d7063000a6c74ffd13 Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Sat, 12 Dec 2020 01:30:15 +0100 Subject: [PATCH 2/8] [WIP] Port Collaboration to CollabWrapper Activity uses telepathy tubes chan, this port aims to move collaboration to collabwrapper. When a user opens the activity, the images shown in the activity are saved in a zip file and shared to other users when the activity is shared. The joined users receive the zip file if they have enough disk space for it. Replace the use of tempfile with activity_zip to save images as a zipfile. Activity currently takes a while to load as load_journal_table searches for images from the root directory and it's subdirectories. TODO - Test collaboration. - Fix issues with zipfile. - Restrict image search to journal objects. Signed-off-by: Ibiam Chihurumnaya --- collabwrapper.py | 887 +++++++++++++++++++++++++++++++++++++++++++++++ readtoolbar.py | 14 +- viewslides.py | 454 +++++++++--------------- 3 files changed, 1055 insertions(+), 300 deletions(-) create mode 100644 collabwrapper.py diff --git a/collabwrapper.py b/collabwrapper.py new file mode 100644 index 0000000..9b01edd --- /dev/null +++ b/collabwrapper.py @@ -0,0 +1,887 @@ +# Copyright (C) 2015 Walter Bender +# Copyright (C) 2015 Sam Parkinson +# +# 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 3 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 library; if not, write to the Free Software +# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA + +''' +The wrapper module provides an abstraction over the Sugar +collaboration system. + +Using CollabWrapper +------------------- +1. Add `get_data` and `set_data` methods to the activity class:: + + def get_data(self): + # return plain python objects - things that can be encoded + # using the json module + return dict( + text=self._entry.get_text() + ) + + def set_data(self, data): + # data will be the same object returned by get_data + self._entry.set_text(data.get('text')) + +2. Make a CollabWrapper instance:: + + def __init__(self, handle): + sugar3.activity.activity.Activity.__init__(self, handle) + self._collab = CollabWrapper(self) + self._collab.connect('message', self.__message_cb) + + # setup your activity here + + self._collab.setup() + +3. Post any changes of shared state to the CollabWrapper. The changes + will be sent to other buddies if any are connected, for example:: + + def __entry_changed_cb(self, *args): + self._collab.post(dict( + action='entry_changed', + new_text=self._entry.get_text() + )) + +4. Handle incoming messages, for example:: + + def __message_cb(self, collab, buddy, msg): + action = msg.get('action') + if action == 'entry_changed': + self._entry.set_text(msg.get('new_text')) + +''' + +import os +import json +import socket +from gettext import gettext as _ + +import gi +gi.require_version('TelepathyGLib', '0.12') +from gi.repository import GObject +from gi.repository import Gio +from gi.repository import GLib +from gi.repository import TelepathyGLib +import dbus +from dbus import PROPERTIES_IFACE + +CHANNEL_INTERFACE = TelepathyGLib.IFACE_CHANNEL +CHANNEL_INTERFACE_GROUP = TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP +CHANNEL_TYPE_TEXT = TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT +CHANNEL_TYPE_FILE_TRANSFER = TelepathyGLib.IFACE_CHANNEL_TYPE_FILE_TRANSFER +CONN_INTERFACE_ALIASING = TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING +CONN_INTERFACE = TelepathyGLib.IFACE_CONNECTION +CHANNEL = TelepathyGLib.IFACE_CHANNEL +CLIENT = TelepathyGLib.IFACE_CLIENT +CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES = \ + TelepathyGLib.ChannelGroupFlags.CHANNEL_SPECIFIC_HANDLES +CONNECTION_HANDLE_TYPE_CONTACT = TelepathyGLib.HandleType.CONTACT +CHANNEL_TEXT_MESSAGE_TYPE_NORMAL = TelepathyGLib.ChannelTextMessageType.NORMAL +SOCKET_ADDRESS_TYPE_UNIX = TelepathyGLib.SocketAddressType.UNIX +SOCKET_ACCESS_CONTROL_LOCALHOST = TelepathyGLib.SocketAccessControl.LOCALHOST + +from sugar3.presence import presenceservice +from sugar3.activity.activity import SCOPE_PRIVATE +from sugar3.graphics.alert import NotifyAlert + +import logging +_logger = logging.getLogger('CollabWrapper') + +ACTION_INIT_REQUEST = '!!ACTION_INIT_REQUEST' +ACTION_INIT_RESPONSE = '!!ACTION_INIT_RESPONSE' +ACTIVITY_FT_MIME = 'x-sugar/from-activity' + + +class CollabWrapper(GObject.GObject): + ''' + The wrapper provides a high level abstraction over the + collaboration system. The wrapper deals with setting up the + channels, encoding and decoding messages, initialization and + alerting the caller to the status. + + An activity instance is initially private, but may be shared. Once + shared, an instance will remain shared for as long as the activity + runs. On stop, the journal will preserve the instance as shared, + and on resume the instance will be shared again. + + When the caller shares an activity instance, they are the leader, + and other buddies may join. The instance is now a shared activity. + + When the caller joins a shared activity, the leader will call + `get_data`, and the caller's `set_data` will be called with the + result. + + The `joined` signal is emitted when the caller joins a shared + activity. One or more `buddy_joined` signals will be emitted before + this signal. The signal is not emitted to the caller who first + shared the activity. There are no arguments. + + The `buddy_joined` signal is emitted when another buddy joins the + shared activity. At least one will be emitted before the `joined` + signal. The caller will never be mentioned, but is assumed to be + part of the set. The signal passes a + :class:`sugar3.presence.buddy.Buddy` as the only argument. + + The `buddy_left` signal is emitted when another user leaves the + shared activity. The signal is not emitted during quit. The signal + passes a :class:`sugar3.presence.buddy.Buddy` as the only argument. + + Any buddy may call `post` to send a message to all buddies. Each + buddy will receive a `message` signal. + + The `message` signal is emitted when a `post` is received from any + buddy. The signal has two arguments. The first is a + :class:`sugar3.presence.buddy.Buddy`. The second is the message. + + Any buddy may call `send_file_memory` or `send_file_file` to + transfer a file to all buddies. A description is to be given. + Each buddy will receive an `incoming_file` signal. + + The `incoming_file` signal is emitted when a file transfer is + received. The signal has two arguments. The first is a + :class:`IncomingFileTransfer`. The second is the description. + ''' + + message = GObject.Signal('message', arg_types=[object, object]) + joined = GObject.Signal('joined') + buddy_joined = GObject.Signal('buddy_joined', arg_types=[object]) + buddy_left = GObject.Signal('buddy_left', arg_types=[object]) + incoming_file = GObject.Signal('incoming_file', arg_types=[object, object]) + + def __init__(self, activity): + _logger.debug('__init__') + GObject.GObject.__init__(self) + self.activity = activity + self.shared_activity = activity.shared_activity + self._leader = False + self._init_waiting = False + self._text_channel = None + self._owner = presenceservice.get_instance().get_owner() + + def setup(self): + ''' + Setup must be called so that the activity can join or share + if appropriate. + + .. note:: + As soon as setup is called, any signal, `get_data` or + `set_data` call may occur. This means that the activity + must have set up enough so these functions can work. For + example, call setup at the end of the activity + `__init__` function. + ''' + _logger.debug('setup') + # Some glue to know if we are launching, joining, or resuming + # a shared activity. + if self.shared_activity: + # We're joining the activity. + self.activity.connect("joined", self.__joined_cb) + + if self.activity.get_shared(): + _logger.debug('calling _joined_cb') + self.__joined_cb(self) + else: + _logger.debug('Joining activity...') + self._alert(_('Joining activity...'), + _('Please wait for the connection...')) + else: + self._leader = True + if not self.activity.metadata or self.activity.metadata.get( + 'share-scope', SCOPE_PRIVATE) == \ + SCOPE_PRIVATE: + # We are creating a new activity instance. + _logger.debug('Off-line') + else: + # We are sharing an old activity instance. + _logger.debug('On-line') + self._alert(_('Resuming shared activity...'), + _('Please wait for the connection...')) + self.activity.connect('shared', self.__shared_cb) + + def _alert(self, title, msg=None): + a = NotifyAlert() + a.props.title = title + a.props.msg = msg + self.activity.add_alert(a) + a.connect('response', lambda a, r: self.activity.remove_alert(a)) + a.show() + + def __shared_cb(self, sender): + ''' Callback for when activity is shared. ''' + _logger.debug('__shared_cb') + # FIXME: may be called twice, but we should only act once + self.shared_activity = self.activity.shared_activity + self._setup_text_channel() + self._listen_for_channels() + + def __joined_cb(self, sender): + '''Callback for when an activity is joined.''' + _logger.debug('__joined_cb') + self.shared_activity = self.activity.shared_activity + if not self.shared_activity: + return + + self._setup_text_channel() + self._listen_for_channels() + self._init_waiting = True + self.post({'action': ACTION_INIT_REQUEST}) + + for buddy in self.shared_activity.get_joined_buddies(): + self.buddy_joined.emit(buddy) + + self.joined.emit() + + def _setup_text_channel(self): + ''' Set up a text channel to use for collaboration. ''' + _logger.debug('_setup_text_channel') + self._text_channel = _TextChannelWrapper( + self.shared_activity.telepathy_text_chan, + self.shared_activity.telepathy_conn) + + # Tell the text channel what callback to use for incoming + # text messages. + self._text_channel.set_received_callback(self.__received_cb) + + # Tell the text channel what callbacks to use when buddies + # come and go. + self.shared_activity.connect('buddy-joined', self.__buddy_joined_cb) + self.shared_activity.connect('buddy-left', self.__buddy_left_cb) + + def _listen_for_channels(self): + _logger.debug('_listen_for_channels') + conn = self.shared_activity.telepathy_conn + conn.connect_to_signal('NewChannels', self.__new_channels_cb) + + def __new_channels_cb(self, channels): + _logger.debug('__new_channels_cb') + conn = self.shared_activity.telepathy_conn + for path, props in channels: + if props[CHANNEL + '.Requested']: + continue # This channel was requested by me + + channel_type = props[CHANNEL + '.ChannelType'] + if channel_type == CHANNEL_TYPE_FILE_TRANSFER: + self._handle_ft_channel(conn, path, props) + + def _handle_ft_channel(self, conn, path, props): + _logger.debug('_handle_ft_channel') + ft = IncomingFileTransfer(conn, path, props) + if ft.description == ACTION_INIT_RESPONSE: + ft.connect('ready', self.__ready_cb) + ft.accept_to_memory() + else: + desc = json.loads(ft.description) + self.incoming_file.emit(ft, desc) + + def __ready_cb(self, ft, stream): + _logger.debug('__ready_cb') + if self._init_waiting: + stream.close(None) + # FIXME: The data prop seems to just be the raw pointer + gbytes = stream.steal_as_bytes() + data = gbytes.get_data() + _logger.debug('Got init data from buddy: %r', data) + data = json.loads(data) + self.activity.set_data(data) + self._init_waiting = False + + def __received_cb(self, buddy, msg): + '''Process a message when it is received.''' + _logger.debug('__received_cb') + action = msg.get('action') + if action == ACTION_INIT_REQUEST: + if self._leader: + data = self.activity.get_data() + if data is not None: + data = json.dumps(data) + OutgoingBlobTransfer( + buddy, + self.shared_activity.telepathy_conn, + data, + self.get_client_name(), + ACTION_INIT_RESPONSE, + ACTIVITY_FT_MIME) + return + + if buddy: + nick = buddy.props.nick + else: + nick = '???' + _logger.debug('Received message from %s: %r', nick, msg) + self.message.emit(buddy, msg) + + def send_file_memory(self, buddy, data, description): + ''' + Send a one to one file transfer from memory to a buddy. The + buddy will get the file transfer and description through the + `incoming_transfer` signal. + + Args: + buddy (sugar3.presence.buddy.Buddy), buddy to send to. + data (str), the data to send. + description (object), a json encodable description for the + transfer. This will be given to the + `incoming_transfer` signal at the buddy. + ''' + OutgoingBlobTransfer( + buddy, + self.shared_activity.telepathy_conn, + data, + self.get_client_name(), + json.dumps(description), + ACTIVITY_FT_MIME) + + def send_file_file(self, buddy, path, description): + ''' + Send a one to one file transfer from a filesystem path to a + given buddy. The buddy will get the file transfer and + description through the `incoming_transfer` signal. + + Args: + buddy (sugar3.presence.buddy.Buddy), buddy to send to. + path (str), path of the file containing the data to send. + description (object), a json encodable description for the + transfer. This will be given to the + `incoming_transfer` signal at the buddy. + ''' + OutgoingFileTransfer( + buddy, + self.shared_activity.telepathy_conn, + path, + self.get_client_name(), + json.dumps(description), + ACTIVITY_FT_MIME) + + def post(self, msg): + ''' + Send a message to all buddies. If the activity is not shared, + no message is sent. + + Args: + msg (object): json encodable object to send, + eg. :class:`dict` or :class:`str`. + ''' + if self._text_channel is not None: + self._text_channel.post(msg) + + def __buddy_joined_cb(self, sender, buddy): + '''A buddy joined.''' + self.buddy_joined.emit(buddy) + + def __buddy_left_cb(self, sender, buddy): + '''A buddy left.''' + self.buddy_left.emit(buddy) + + def get_client_name(self): + ''' + Get the name of the activity's telepathy client. + + Returns: str, telepathy client name + ''' + return CLIENT + '.' + self.activity.get_bundle_id() + + @GObject.Property + def leader(self): + ''' + Boolean of if this client is the leader in this activity. The + way the leader is decided may change, however there should only + ever be one leader for an activity. + ''' + return self._leader + + @GObject.Property + def owner(self): + ''' + Ourselves, :class:`sugar3.presence.buddy.Owner` + ''' + return self._owner + + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +FT_REASON_NONE = 0 +FT_REASON_REQUESTED = 1 +FT_REASON_LOCAL_STOPPED = 2 +FT_REASON_REMOTE_STOPPED = 3 +FT_REASON_LOCAL_ERROR = 4 +FT_REASON_LOCAL_ERROR = 5 +FT_REASON_REMOTE_ERROR = 6 + + +class _BaseFileTransfer(GObject.GObject): + ''' + The base file transfer should not be used directly. It is used as a + base class for the incoming and outgoing file transfers. + + Props: + filename (str), metadata provided by the buddy + file_size (str), size of the file being sent/received, in bytes + description (str), metadata provided by the buddy + mime_type (str), metadata provided by the buddy + buddy (:class:`sugar3.presence.buddy.Buddy`), other party + in the transfer + reason_last_change (FT_REASON_*), reason for the last state change + + GObject Props: + state (FT_STATE_*), current state of the transfer + transferred_bytes (int), number of bytes transferred so far + ''' + + def __init__(self): + GObject.GObject.__init__(self) + self._state = FT_STATE_NONE + self._transferred_bytes = 0 + + self.channel = None + self.buddy = None + self.filename = None + self.file_size = None + self.description = None + self.mime_type = None + self.reason_last_change = FT_REASON_NONE + + def set_channel(self, channel): + ''' + Setup the file transfer to use a given telepathy channel. This + should only be used by direct subclasses of the base file transfer. + ''' + 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[PROPERTIES_IFACE] + + props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER) + self._state = props['State'] + self.filename = props['Filename'] + self.file_size = props['Size'] + self.description = props['Description'] + self.mime_type = props['ContentType'] + + def __transferred_bytes_changed_cb(self, transferred_bytes): + _logger.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): + _logger.debug('__initial_offset_defined_cb %r', offset) + self.initial_offset = offset + + def __state_changed_cb(self, state, reason): + _logger.debug('__state_changed_cb %r %r', state, reason) + self.reason_last_change = 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): + ''' + Request that telepathy close the file transfer channel + + Spec: http://telepathy.freedesktop.org/spec/Channel.html#Method:Close + ''' + self.channel[CHANNEL].Close() + + +class IncomingFileTransfer(_BaseFileTransfer): + ''' + An incoming file transfer from another buddy. You need to first accept + the transfer (either to memory or to a file). Then you need to listen + to the state and wait until the transfer is completed. Then you can + read the file that it was saved to, or access the + :class:`Gio.MemoryOutputStream` from the `output` property. + + The `output` property is different depending on how the file was accepted. + If the file was accepted to a file on the file system, it is a string + representing the path to the file. If the file was accepted to memory, + it is a :class:`Gio.MemoryOutputStream`. + ''' + + ready = GObject.Signal('ready', arg_types=[object]) + + def __init__(self, connection, object_path, props): + _BaseFileTransfer.__init__(self) + + channel = {} + proxy = dbus.Bus().get_object(connection.bus_name, object_path) + channel[PROPERTIES_IFACE] = dbus.Interface(proxy, PROPERTIES_IFACE) + channel[CHANNEL] = dbus.Interface(proxy, CHANNEL) + channel[CHANNEL_TYPE_FILE_TRANSFER] = dbus.Interface( + proxy, CHANNEL_TYPE_FILE_TRANSFER) + self.set_channel(channel) + + self.connect('notify::state', self.__notify_state_cb) + + self._destination_path = None + self._output_stream = None + self._socket_address = None + self._socket = None + self._splicer = None + + def accept_to_file(self, destination_path): + ''' + Accept the file transfer and write it to a new file. The file must + already exist. + + Args: + destination_path (str): the path where a new file will be + created and saved to + ''' + if os.path.exists(destination_path): + raise ValueError('Destination path already exists: %r' % + destination_path) + + self._destination_path = destination_path + self._accept() + + def accept_to_memory(self): + ''' + Accept the file transfer. Once the state is FT_STATE_OPEN, a + :class:`Gio.MemoryOutputStream` accessible via the output prop. + ''' + self._destination_path = None + self._accept() + + def _accept(self): + channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_ft.AcceptFile( + SOCKET_ADDRESS_TYPE_UNIX, + SOCKET_ACCESS_CONTROL_LOCALHOST, + '', + 0, + byte_arrays=True) + + def __notify_state_cb(self, file_transfer, pspec): + _logger.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.UnixInputStream.new(self._socket.fileno(), True) + + if self._destination_path is not None: + destination_file = Gio.File.new_for_path( + self._destination_path) + if self.initial_offset == 0: + self._output_stream = destination_file.create( + Gio.FileCreateFlags.PRIVATE, None) + else: + self._output_stream = destination_file.append_to() + else: + if hasattr(Gio.MemoryOutputStream, 'new_resizable'): + self._output_stream = \ + Gio.MemoryOutputStream.new_resizable() + else: + self._output_stream = Gio.MemoryOutputStream() + + self._output_stream.splice_async( + input_stream, + Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | + Gio.OutputStreamSpliceFlags.CLOSE_TARGET, + GLib.PRIORITY_LOW, None, self.__splice_done_cb, None) + + def __splice_done_cb(self, output_stream, res, user): + _logger.debug('__splice_done_cb') + self.ready.emit(self._destination_path or self._output_stream) + + @GObject.Property + def output(self): + return self._destination_path or self._output_stream + + @GObject.Property + def socket_address(self): + return self._socket_address + + +class _BaseOutgoingTransfer(_BaseFileTransfer): + ''' + This class provides the base of an outgoing file transfer. + + You can override the `_get_input_stream` method to return any type of + Gio input stream. This will then be used to provide the file if + requested by the application. You also need to call `_create_channel` + with the length of the file in bytes during your `__init__`. + + Args: + buddy (sugar3.presence.buddy.Buddy), who to send the transfer to + conn (telepathy.client.conn.Connection), telepathy connection to + use to send the transfer. Eg. `shared_activity.telepathy_conn` + filename (str), metadata sent to the receiver + description (str), metadata sent to the receiver + mime (str), metadata sent to the receiver + ''' + + def __init__(self, buddy, conn, filename, description, mime): + _BaseFileTransfer.__init__(self) + self.connect('notify::state', self.__notify_state_cb) + + self._socket_address = None + self._socket = None + self._splicer = None + self._conn = conn + self._filename = filename + self._description = description + self._mime = mime + self.buddy = buddy + + def _create_channel(self, file_size): + object_path, properties_ = self._conn.CreateChannel(dbus.Dictionary({ + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + CHANNEL + '.TargetHandle': self.buddy.contact_handle, + CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self._filename, + CHANNEL_TYPE_FILE_TRANSFER + '.Description': self._description, + CHANNEL_TYPE_FILE_TRANSFER + '.Size': file_size, + CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self._mime, + CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0}, signature='sv')) + channel = {} + proxy = dbus.Bus().get_object(self._conn.bus_name, object_path) + channel[PROPERTIES_IFACE] = dbus.Interface(proxy, PROPERTIES_IFACE) + channel[CHANNEL] = dbus.Interface(proxy, CHANNEL) + channel[CHANNEL_TYPE_FILE_TRANSFER] = dbus.Interface( + proxy, CHANNEL_TYPE_FILE_TRANSFER) + self.set_channel(channel) + + channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_file_transfer.ProvideFile( + SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '', + byte_arrays=True) + + def _get_input_stream(self): + raise NotImplementedError() + + def __notify_state_cb(self, file_transfer, pspec): + 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.UnixOutputStream.new( + self._socket.fileno(), True) + + input_stream = self._get_input_stream() + output_stream.splice_async( + input_stream, + Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | + Gio.OutputStreamSpliceFlags.CLOSE_TARGET, + GLib.PRIORITY_LOW, None, None, None) + + +class OutgoingFileTransfer(_BaseOutgoingTransfer): + ''' + An outgoing file transfer to send from a file (on the computer's file + system). + + Note that the `path` argument is the path for the file that will be + sent, whereas the `filename` argument is only for metadata. + + Args: + path (str), path of the file to send + ''' + + def __init__(self, buddy, conn, path, filename, description, mime): + _BaseOutgoingTransfer.__init__( + self, buddy, conn, filename, description, mime) + + self._path = path + file_size = os.stat(path).st_size + self._create_channel(file_size) + + def _get_input_stream(self): + return Gio.File.new_for_path(self._path).read(None) + + +class OutgoingBlobTransfer(_BaseOutgoingTransfer): + ''' + An outgoing file transfer to send from a string in memory. + + Args: + blob (str), data to send + ''' + + def __init__(self, buddy, conn, blob, filename, description, mime): + _BaseOutgoingTransfer.__init__( + self, buddy, conn, filename, description, mime) + + self._blob = blob.encode('utf-8') + self._create_channel(len(self._blob)) + + def _get_input_stream(self): + return Gio.MemoryInputStream.new_from_data(self._blob, None) + + +class _TextChannelWrapper(object): + '''Wrapper for a telepathy Text Channel''' + + def __init__(self, text_chan, conn): + '''Connect to the text channel''' + self._activity_cb = None + self._activity_close_cb = None + self._text_chan = text_chan + self._conn = conn + self._signal_matches = [] + m = self._text_chan[CHANNEL_INTERFACE].connect_to_signal( + 'Closed', self._closed_cb) + self._signal_matches.append(m) + + def post(self, msg): + if msg is not None: + _logger.debug('post') + self._send(json.dumps(msg)) + + def _send(self, text): + '''Send text over the Telepathy text channel.''' + _logger.debug('sending %s' % text) + + if self._text_chan is not None: + self._text_chan[CHANNEL_TYPE_TEXT].Send( + CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text) + + def close(self): + '''Close the text channel.''' + _logger.debug('Closing text channel') + try: + self._text_chan[CHANNEL_INTERFACE].Close() + except Exception: + _logger.debug('Channel disappeared!') + self._closed_cb() + + def _closed_cb(self): + '''Clean up text channel.''' + for match in self._signal_matches: + match.remove() + self._signal_matches = [] + self._text_chan = None + if self._activity_close_cb is not None: + self._activity_close_cb() + + def set_received_callback(self, callback): + '''Connect the function callback to the signal. + + callback -- callback function taking buddy and text args + ''' + if self._text_chan is None: + return + self._activity_cb = callback + m = self._text_chan[CHANNEL_TYPE_TEXT].connect_to_signal( + 'Received', self._received_cb) + self._signal_matches.append(m) + + def handle_pending_messages(self): + '''Get pending messages and show them as received.''' + for identity, timestamp, sender, type_, flags, text in \ + self._text_chan[ + CHANNEL_TYPE_TEXT].ListPendingMessages(False): + self._received_cb(identity, timestamp, sender, type_, flags, text) + + def _received_cb(self, identity, timestamp, sender, type_, flags, text): + '''Handle received text from the text channel. + + Converts sender to a Buddy. + Calls self._activity_cb which is a callback to the activity. + ''' + _logger.debug('received_cb %r %s' % (type_, text)) + if type_ != 0: + # Exclude any auxiliary messages + return + + msg = json.loads(text) + + if self._activity_cb: + try: + self._text_chan[CHANNEL_INTERFACE_GROUP] + except Exception: + # One to one XMPP chat + nick = self._conn[ + CONN_INTERFACE_ALIASING].RequestAliases([sender])[0] + buddy = {'nick': nick, 'color': '#000000,#808080'} + _logger.debug('exception: received from sender %r buddy %r' % + (sender, buddy)) + else: + # XXX: cache these + buddy = self._get_buddy(sender) + _logger.debug('Else: received from sender %r buddy %r' % + (sender, buddy)) + + self._activity_cb(buddy, msg) + self._text_chan[ + CHANNEL_TYPE_TEXT].AcknowledgePendingMessages([identity]) + else: + _logger.debug('Throwing received message on the floor' + ' since there is no callback connected. See' + ' set_received_callback') + + def set_closed_callback(self, callback): + '''Connect a callback for when the text channel is closed. + + callback -- callback function taking no args + + ''' + _logger.debug('set closed callback') + self._activity_close_cb = callback + + def _get_buddy(self, cs_handle): + '''Get a Buddy from a (possibly channel-specific) handle.''' + # XXX This will be made redundant once Presence Service + # provides buddy resolution + + # Get the Presence Service + pservice = presenceservice.get_instance() + + # Get the Telepathy Connection + tp_name, tp_path = pservice.get_preferred_connection() + obj = dbus.Bus().get_object(tp_name, tp_path) + conn = dbus.Interface(obj, CONN_INTERFACE) + group = self._text_chan[CHANNEL_INTERFACE_GROUP] + my_csh = group.GetSelfHandle() + if my_csh == cs_handle: + handle = conn.GetSelfHandle() + elif group.GetGroupFlags() & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + else: + handle = cs_handle + + # XXX: deal with failure to get the handle owner + assert handle != 0 + + return pservice.get_buddy_by_telepathy_handle( + tp_name, tp_path, handle) diff --git a/readtoolbar.py b/readtoolbar.py index a3c7fe0..2e05089 100644 --- a/readtoolbar.py +++ b/readtoolbar.py @@ -293,17 +293,29 @@ def set_activity(self, activity): self.activity = activity def _reload_journal_table_cb(self, button): - self.activity.load_journal_table() + self.activity.reload_journal_table() + + if self.activity.get_shared(): + self.activity.collab.post(dict(action="reload")) def _add_image_cb(self, button): self.activity.add_image() + if self.activity.get_shared(): + self.activity.collab.post(dict(action="add-image")) + def _remove_image_cb(self, button): self.activity.remove_image() + if self.activity.get_shared(): + self.activity.collab.post(dict(action="remove-image")) + def extract_image_cb(self, button): self.activity.extract_image() + if self.activity.get_shared(): + self.activity.collab.post(dict(action="extract")) + def _show_image_tables_cb(self, button): self._hide_image_tables.props.sensitive = True self._reload_journal_table.props.sensitive = True diff --git a/viewslides.py b/viewslides.py index bd9598b..edeea98 100644 --- a/viewslides.py +++ b/viewslides.py @@ -51,6 +51,7 @@ import pickle from decimal import * import xopower +from collabwrapper import CollabWrapper _TOOLBAR_READ = 1 _TOOLBAR_SLIDES = 3 @@ -147,51 +148,6 @@ def save(self): pickle_output.close() -class ReadHTTPRequestHandler(network.ChunkedGlibHTTPRequestHandler): - """HTTP Request Handler for transferring document while collaborating. - - RequestHandler class that integrates with Glib mainloop. It writes - the specified file to the client in chunks, returning control to the - mainloop between chunks. - - """ - - def translate_path(self, path): - """Return the filepath to the shared document.""" - return self.server.filepath - - -class ReadHTTPServer(network.GlibTCPServer): - """HTTP Server for transferring document while collaborating.""" - - def __init__(self, server_address, filepath): - """Set up the GlibTCPServer with the ReadHTTPRequestHandler. - - filepath -- path to shared document to be served. - """ - self.filepath = filepath - network.GlibTCPServer.__init__(self, server_address, - ReadHTTPRequestHandler) - - -class ReadURLDownloader(network.GlibURLDownloader): - """URLDownloader that provides content-length and content-type.""" - - def get_content_length(self): - """Return the content-length of the download.""" - if self._info is not None: - return int(self._info.headers.get('Content-Length')) - - def get_content_type(self): - """Return the content-type of the download.""" - if self._info is not None: - return self._info.headers.get('Content-type') - return None - - -READ_STREAM_SERVICE = 'viewslides-activity-http' - - class ViewSlidesActivity(activity.Activity): __gsignals__ = { 'go-fullscreen': (GObject.SIGNAL_RUN_FIRST, @@ -207,6 +163,7 @@ def __init__(self, handle): self._object_id = handle.object_id self.zoom_image_to_fit = True self.total_pages = 0 + self.buddies = {} self.connect("draw", self.__draw_cb) self.connect("delete-event", self.__delete_event_cb) @@ -237,7 +194,7 @@ def __init__(self, handle): self.sidebar = Sidebar() self.sidebar.show() - self.ls_left = self.ls_left = Gtk.ListStore( + self.ls_left = Gtk.ListStore( GObject.TYPE_STRING, GObject.TYPE_STRING) tv_left = Gtk.TreeView(self.ls_left) tv_left.set_rules_hint(True) @@ -308,6 +265,10 @@ def __init__(self, handle): self.is_dirty = False self.annotations_dirty = False + self.activity_zip = os.path.join( + self.get_activity_root(), + 'instance', + 'viewslides-files') self.load_journal_table() self.show_image("ViewSlides.jpg") @@ -333,40 +294,27 @@ def __init__(self, handle): self.connect("focus-out-event", self._focus_out_event_cb) self.connect("notify::active", self._now_active_cb) - self.unused_download_tubes = set() - self._want_document = True - self._download_content_length = 0 - self._download_content_type = None + self._want_document = False # Status of temp file used for write_file: - self.tempfile = None self._close_requested = False self.connect("shared", self._shared_cb) - h = hash(self._activity_id) - self.port = 1024 + (h % 64511) self.is_received_document = False self.selected_journal_entry = None self.selected_title = None self.selection_left = None - if self.shared_activity and handle.object_id is None: - # We're joining, and we don't already have the document. - if self.get_shared(): - # Already joined for some reason, just get the document - self._joined_cb(self) - else: - # Wait for a successful join before trying to get the document - self.connect("joined", self._joined_cb) - else: - # Assign a file path to create if one doesn't exist yet + if not self.get_shared(): if handle.object_id is None: - self.tempfile = os.path.join( - self.get_activity_root(), - 'instance', - 'tmp{}'.format(time.time())) - self.show_image_tables(True) + self.collab = CollabWrapper(self) + self.collab.joined.connect(self._joined_cb) + self.collab.buddy_joined.connect(self._buddy_joined_cb) + self.collab.buddy_left.connect(self._buddy_left_cb) + self.collab.incoming_file.connect(self._incoming_file_cb) + self.collab.setup() + def create_new_toolbar(self): toolbar_box = ToolbarBox() @@ -499,6 +447,12 @@ def create_new_toolbar(self): # Not joining, not resuming slides_toolbar_button.set_expanded(True) + def get_data(self): + return None + + def set_data(self, data): + pass + def _zoom_in_cb(self, button): self._zoom_in.props.sensitive = False self._zoom_out.props.sensitive = True @@ -616,6 +570,7 @@ def load_journal_table(self): '.TIFF', '.png', '.PNG') + self.activity_zip = zipfile.ZipFile(self.activity_zip, 'w') for dirname, dirnames, filenames in os.walk('/media'): if '.olpc.store' in dirnames: # don't visit .olpc.store directories @@ -628,9 +583,25 @@ def load_journal_table(self): os.path.join(dirname, filename)) self.ls_right.set(iter, COLUMN_IMAGE, filename) self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) + if filename not in self.activity_zip.namelist(): + self.activity_zip.write(os.path.join(dirname, filename), filename) + self.activity_zip.close() self.ls_right.set_sort_column_id(COLUMN_IMAGE, Gtk.SortType.ASCENDING) + def reload_journal_table(self): + if os.path.exists(self.activity_zip.filename) and zipfile.is_zipfile(self.activity_zip): + zf = zipfile.ZipFile(self.activity_zip, 'r') + for filename in zf.namelist(): + iter = self.ls_right.append() + jobject_wrapper = JobjectWrapper() + jobject_wrapper.set_file_path( + os.path.abspath(self.activity_zip.filename)) + self.ls_right.set(iter, COLUMN_IMAGE, filename) + self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) + else: + self.load_journal_table() + def col_left_edited_cb(self, cell, path, new_text, user_data): liststore = user_data if self.check_for_duplicates(new_text): @@ -660,7 +631,7 @@ def show_image_tables(self, state): self.sidebar.show() self.rewrite_zip() self.set_current_page(0) - self._load_document(self.tempfile) + self._load_document(os.path.abspath(self.activity_zip)) def selection_left_cb(self, selection): tv = selection.get_tree_view() @@ -669,7 +640,7 @@ def selection_left_cb(self, selection): if self.selection_left: model, iter = self.selection_left selected_file = model.get_value(iter, COLUMN_OLD_NAME) - zf = zipfile.ZipFile(self.tempfile, 'r') + zf = zipfile.ZipFile(self.activity_zip, 'r') if self.save_extracted_file(zf, selected_file): fname = os.path.join( self.get_activity_root(), @@ -705,28 +676,16 @@ def add_image(self): str(arcname) + ' already exists in slideshow!') return - # Assign a file path to create if one doesn't exist yet - if self.tempfile is None: - self.tempfile = os.path.join(self.get_activity_root(), 'instance', - 'tmp{}'.format(time.time())) - try: - if os.path.exists(self.tempfile): - zf = zipfile.ZipFile(self.tempfile, 'a') - else: - zf = zipfile.ZipFile(self.tempfile, 'w') - zf.write(selected_file.encode("utf-8"), arcname.encode("utf-8")) - zf.close() - iter = self.ls_left.append() - self.ls_left.set( - iter, - COLUMN_IMAGE, - arcname, - COLUMN_OLD_NAME, - arcname) - self._slides_toolbar._add_image.props.sensitive = False - except BadZipfile as err: - print('Error opening the zip file: {}'.format(err)) - self._alert('Error', 'Error opening the zip file') + + iter = self.ls_left.append() + self.ls_left.set( + iter, + COLUMN_IMAGE, + arcname, + COLUMN_OLD_NAME, + arcname) + + self._slides_toolbar._add_image.props.sensitive = False def remove_image(self): if self.selection_left: @@ -739,7 +698,7 @@ def extract_image(self): if self.selection_left: model, iter = self.selection_left selected_file = model.get_value(iter, COLUMN_OLD_NAME) - zf = zipfile.ZipFile(self.tempfile, 'r') + zf = zipfile.ZipFile(self.activity_zip, 'r') if self.save_extracted_file(zf, selected_file): fname = os.path.join( self.get_activity_root(), @@ -781,10 +740,10 @@ def rewrite_zip(self): if not self.is_dirty: return new_zipfile = os.path.join(self.get_activity_root(), 'instance', - 'rewrite{}'.format(time.time())) - print(self.tempfile, new_zipfile) + 'activity_zip_rewrite') + _logger.debug(self.activity_zip, new_zipfile) zf_new = zipfile.ZipFile(new_zipfile, 'w') - zf_old = zipfile.ZipFile(self.tempfile, 'r') + zf_old = zipfile.ZipFile(self.activity_zip, 'r') for row in self.ls_left: copied_file = row[COLUMN_OLD_NAME] new_file = row[COLUMN_IMAGE] @@ -797,8 +756,8 @@ def rewrite_zip(self): os.remove(fname) zf_old.close() zf_new.close() - os.remove(self.tempfile) - self.tempfile = new_zipfile + os.remove(self.activity_zip) + self.activity_zip = new_zipfile self.is_dirty = False def final_rewrite_zip(self): @@ -807,9 +766,9 @@ def final_rewrite_zip(self): new_zipfile = os.path.join(self.get_activity_root(), 'instance', 'rewrite{}'.format(time.time())) - print(self.tempfile, new_zipfile) + print(self.activity_zip, new_zipfile) zf_new = zipfile.ZipFile(new_zipfile, 'w') - zf_old = zipfile.ZipFile(self.tempfile, 'r') + zf_old = zipfile.ZipFile(self.activity_zip, 'r') image_files = zf_old.namelist() i = 0 while (i < len(image_files)): @@ -825,8 +784,8 @@ def final_rewrite_zip(self): zf_old.close() zf_new.close() - os.remove(self.tempfile) - self.tempfile = new_zipfile + os.remove(self.activity_zip) + self.activity_zip = new_zipfile def __button_press_event_cb(self, widget, event): widget.grab_focus() @@ -1121,11 +1080,11 @@ def save_extracted_file(self, zipfile, filename): if (outfn == ''): return False fname = os.path.join(self.get_activity_root(), 'instance', outfn) - f = open(fname, 'w') + f = open(fname, 'wb') try: f.write(filebytes) finally: - f.close + f.close() return True def extract_pickle_file(self): @@ -1137,21 +1096,15 @@ def extract_pickle_file(self): try: f.write(filebytes) finally: - f.close + f.close() return True except KeyError: return False def read_file(self, file_path): """Load a file from the datastore on activity start""" - tempfile = os.path.join( - self.get_activity_root(), - 'instance', - 'tmp{}'.format(time.time())) - os.link(file_path, tempfile) - self.tempfile = tempfile self.get_saved_page_number() - self._load_document(self.tempfile) + self._load_document(self.activity_zip) def __delete_event_cb(self, widget, event): os.remove(self.temp_filename) @@ -1197,32 +1150,19 @@ def _load_document(self, file_path): self.zf = zipfile.ZipFile(file_path, 'r') self.image_files = self.zf.namelist() self.image_files.sort() - i = 0 - valid_endings = ( - '.jpg', - '.jpeg', - '.JPEG', - '.JPG', - '.gif', - '.GIF', - '.tiff', - '.TIFF', - '.png', - '.PNG') self.ls_left.clear() + i = 0 while i < len(self.image_files): newfn = self.make_new_filename(self.image_files[i]) - if newfn.endswith(valid_endings): - iter = self.ls_left.append() - self.ls_left.set( - iter, - COLUMN_IMAGE, - self.image_files[i], - COLUMN_OLD_NAME, - self.image_files[i]) - i = i + 1 - else: - del self.image_files[i] + iter = self.ls_left.append() + self.ls_left.set( + iter, + COLUMN_IMAGE, + self.image_files[i], + COLUMN_OLD_NAME, + self.image_files[i]) + i += 1 + self.extract_pickle_file() self.annotations.restore() self.show_page(self.page) @@ -1231,22 +1171,14 @@ def _load_document(self, file_path): if self.is_received_document: self.metadata['title'] = self.annotations.get_title() self.metadata['title_set_by_user'] = '1' - # We've got the document, so if we're a shared activity, offer it - if self.get_shared(): - self.watch_for_tubes() - self._share_document() else: print('Not a zipfile', file_path) - self.tempfile = None + self.activity_zip = None def write_file(self, file_path): "Save meta data for the file." - # Assign a file path to create if one doesn't exist yet - if self.tempfile is None: - self.tempfile = os.path.join(self.get_activity_root(), 'instance', - 'tmp{}'.format(time.time())) - if not os.path.exists(self.tempfile): - zf = zipfile.ZipFile(self.tempfile, 'w') + if not os.path.exists(self.activity_zip): + zf = zipfile.ZipFile(self.activity_zip, 'w') zf.writestr("filler.txt", "filler") zf.close() @@ -1267,12 +1199,11 @@ def write_file(self, file_path): self.annotations.set_title(str(title)) self.annotations.save() self.final_rewrite_zip() - os.link(self.tempfile, file_path) - _logger.debug( - "Removing temp file {} because we will close".format(self.tempfile)) - os.unlink(self.tempfile) + os.link( + os.path.abspath(self.activity_zip), file_path) + os.unlink(os.path.abspath(self.activity_zip)) os.remove(self.pickle_file_temp) - self.tempfile = None + self.activity_zip = None self.pickle_file_temp = None def can_close(self): @@ -1281,188 +1212,113 @@ def can_close(self): # The code from here on down is for sharing. def set_downloaded_bytes(self, bytes, total): + _logger.debug("Downloaded {} of {} bytes...".format( + bytes_downloaded, + fsize, + )) fraction = float(bytes) / float(total) self.progressbar.set_fraction(fraction) def clear_downloaded_bytes(self): self.progressbar.set_fraction(0.0) - def _download_result_cb(self, getter, tempfile, suggested_name, tube_id): - if self._download_content_type == 'text/html': - # got an error page instead - self._download_error_cb(getter, 'HTTP Error', tube_id) - return - - del self.unused_download_tubes - - self.tempfile = tempfile - file_path = os.path.join(self.get_activity_root(), 'instance', - '{}'.format(time.time())) - _logger.debug("Saving file {} to datastore...".format(file_path)) - os.link(tempfile, file_path) - self._jobject.file_path = file_path + def _download_complete_cb(self, fpath): + _logger.debug( + "Saving file {} to datastore...".format( + os.path.basename(fpath))) + self._jobject.file_path = fpath datastore.write(self._jobject, transfer_ownership=True) - _logger.debug("Got document {} ({}) from tube {}".format( - tempfile, suggested_name, tube_id)) - self._load_document(tempfile) + self._load_document(fpath) self.save() self.progressbar.hide() + self.clear_downloaded_bytes() - def _download_progress_cb(self, getter, bytes_downloaded, tube_id): - if self._download_content_length > 0: - _logger.debug("Downloaded {} of {} bytes from tube {}...".format( - bytes_downloaded, self._download_content_length, - tube_id)) - else: - _logger.debug("Downloaded {} bytes from tube {}...".format( - bytes_downloaded, tube_id)) - total = self._download_content_length - self.set_downloaded_bytes(bytes_downloaded, total) - Gdk.threads_enter() - while Gtk.events_pending(): - Gtk.main_iteration() - Gdk.threads_leave() - - def _download_error_cb(self, getter, err, tube_id): - self.progressbar.hide() - _logger.debug("Error getting document from tube {}: {}".format( - tube_id, err)) - self._alert('Failure', 'Error getting document from tube') + def _buddy_joined_cb(self, collab, buddy): self._want_document = True - self._download_content_length = 0 - self._download_content_type = None - GLib.idle_add(self._get_document) - - def _download_document(self, tube_id, path): - # FIXME: should ideally have the CM listen on a Unix socket - # instead of IPv4 (might be more compatible with Rainbow) - chan = self.shared_activity.telepathy_tubes_chan - iface = chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES] - addr = iface.AcceptStreamTube( - tube_id, - TelepathyGLib.SocketAddressType.IPV4, - TelepathyGLib.SocketAccessControl.LOCALHOST, - 0, - utf8_strings=True) - _logger.debug('Accepted stream tube: listening address is {}'.format(addr)) - # SOCKET_ADDRESS_TYPE_IPV4 is defined to have addresses of type '(sq)' - assert isinstance(addr, dbus.Struct) - assert len(addr) == 2 - assert isinstance(addr[0], str) - assert isinstance(addr[1], int) - assert addr[1] > 0 and addr[1] < 65536 - port = int(addr[1]) - - getter = ReadURLDownloader("http://{}:{}/document".format( - addr[0], port)) - getter.connect("finished", self._download_result_cb, tube_id) - getter.connect("progress", self._download_progress_cb, tube_id) - getter.connect("error", self._download_error_cb, tube_id) - _logger.debug("Starting download to {}...".format(path)) - getter.start(path) - self._download_content_length = getter.get_content_length() - self._download_content_type = getter.get_content_type() - return False + if buddy.nick in self.buddies.keys(): + return + self.buddies[buddy.nick] = buddy - def _get_document(self): + GLib.idle_add(self._share_document, buddy) + + def _buddy_left_cb(self, collab, buddy): + if buddy.nick not in self.buddies.keys(): + return + del self.buddies[buddy.nick] + + def _message_cb(self, collab, buddy, message): + action = message.get('action') + + if action == 'add-image': + self.add_image() + if action == 'remove-image': + self.remove_image() + if action == 'extract': + self.extract_image() + if action == 'reload': + self.reload_journal_table() + + def _incoming_file_cb(self, collab, incoming_file, description): + _logger.debug("Accepting incoming file") if not self._want_document: return False - # Assign a file path to download if one doesn't exist yet - if not self._jobject.file_path: - path = os.path.join(self.get_activity_root(), 'instance', - 'tmp{}'.format(time.time())) + # Assign a file path to write incoming file to + fpath = os.path.join(self.get_activity_root(), 'instance', + 'activity-files') + + # Make sure the receiver has enough space on their + # computer to receive the sent file + def diskfree(path): + return os.statvfs(path).f_bsize * os.statvfs(path).f_bavail + + a_space = diskfree(os.path.dirname(fpath)) // (1024**2) + + if a_space > incoming_file.file_size: + incoming_file.accept_to_file(fpath) + incoming_file.ready.connect(ready_cb) + transfer_complete = False + + while not transfer_complete: + self.progressbar.show() + self.set_downloaded_bytes( + incoming_file.props.transferred_bytes, + incoming_file.props.file_size) else: - path = self._jobject.file_path + self._alert("No space on disk", + "Delete some files to create space") + self._want_document = True + incoming_file.cancel() - # Pick an arbitrary tube we can try to download the document from - try: - tube_id = self.unused_download_tubes.pop() - except (ValueError, KeyError) as Error: - _logger.debug('No tubes to get the document from right now: {}'.format( - Error)) - return False + def ready_cb(fpath): + transfer_complete = True + self._download_complete_cb(fpath) # Avoid trying to download the document multiple times at once self._want_document = False - self.progressbar.show() - GLib.idle_add(self._download_document, tube_id, path) - return False def _joined_cb(self, also_self): """Callback for when a shared activity is joined. Get the shared document from another participant. """ - self.watch_for_tubes() - GLib.idle_add(self._get_document) + _logger.debug('Activity has been joined') - def _share_document(self): + def _share_document(self, buddy): """Share the document.""" - # FIXME: should ideally have the fileserver listen on a Unix socket - # instead of IPv4 (might be more compatible with Rainbow) - - _logger.debug('Starting HTTP server on port {}'.format(self.port)) - self._fileserver = ReadHTTPServer(("", self.port), - self.tempfile) - - # Make a tube for it - chan = self.shared_activity.telepathy_tubes_chan - iface = chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES] - self._fileserver_tube_id = iface.OfferStreamTube( - READ_STREAM_SERVICE, - {}, - TelepathyGLib.SocketAddressType.IPV4, - ('127.0.0.1', - dbus.UInt16( - self.port)), - TelepathyGLib.SocketAccessControl.LOCALHOST, - 0) - - def watch_for_tubes(self): - """Watch for new tubes.""" - tubes_chan = self.shared_activity.telepathy_tubes_chan - - tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].connect_to_signal( - 'NewTube', self._new_tube_cb) - tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].ListTubes( - reply_handler=self._list_tubes_reply_cb, - error_handler=self._list_tubes_error_cb) - - def _new_tube_cb(self, tube_id, initiator, tube_type, service, params, - state): - """Callback when a new tube becomes available.""" - _logger.debug('New tube: ID={} initator={} type={} service={} ' - 'params={} state={}'.format(tube_id, initiator, tube_type, - service, params, state)) - if service == READ_STREAM_SERVICE: - _logger.debug('I could download from that tube') - self.unused_download_tubes.add(tube_id) - # if no download is in progress, let's fetch the document - if self._want_document: - GLib.idle_add(self._get_document) - - def _list_tubes_reply_cb(self, tubes): - """Callback when new tubes are available.""" - for tube_info in tubes: - self._new_tube_cb(*tube_info) - - def _list_tubes_error_cb(self, error): - """Handle ListTubes error by logging.""" - _logger.error('ListTubes() failed: {}'.format(error)) + if self._want_document: + path = os.path.abspath(self.activity_zip.filename) + self.collab.send_file_file( + buddy, + path, + "Share zip file to joined buddies") def _shared_cb(self, activityid): - """Callback when activity shared. - - Set up to share the document. - - """ + """Callback when activity shared.""" # We initiated this activity and have now shared it, so by # definition we have the file. _logger.debug('Activity became shared') - self.watch_for_tubes() - self._share_document() def _alert(self, title, text=None): alert = NotifyAlert(timeout=15) From 9368aff0b32121221220c646ce07830334b539ab Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Tue, 15 Dec 2020 14:34:09 +0100 Subject: [PATCH 3/8] Restrict images search to journal objects. Activity load time has reduced. Signed-off-by: Ibiam Chihurumnaya --- viewslides.py | 37 +++++++------------------------------ 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/viewslides.py b/viewslides.py index edeea98..0000a24 100644 --- a/viewslides.py +++ b/viewslides.py @@ -538,6 +538,7 @@ def load_journal_table(self): ds_objects, num_objects = datastore.find({'mime_type': ['image/jpeg', 'image/gif', 'image/tiff', 'image/png']}, properties=['uid', 'title', 'mime_type']) self.ls_right.clear() + self.activity_zip = zipfile.ZipFile(self.activity_zip, 'w') for i in range(0, num_objects, 1): iter = self.ls_right.append() title = ds_objects[i].metadata['title'] @@ -559,34 +560,10 @@ def load_journal_table(self): jobject_wrapper.set_jobject(ds_objects[i]) self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) - valid_endings = ( - '.jpg', - '.jpeg', - '.JPEG', - '.JPG', - '.gif', - '.GIF', - '.tiff', - '.TIFF', - '.png', - '.PNG') - self.activity_zip = zipfile.ZipFile(self.activity_zip, 'w') - for dirname, dirnames, filenames in os.walk('/media'): - if '.olpc.store' in dirnames: - # don't visit .olpc.store directories - dirnames.remove('.olpc.store') - for filename in filenames: - if filename.endswith(valid_endings): - iter = self.ls_right.append() - jobject_wrapper = JobjectWrapper() - jobject_wrapper.set_file_path( - os.path.join(dirname, filename)) - self.ls_right.set(iter, COLUMN_IMAGE, filename) - self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) - if filename not in self.activity_zip.namelist(): - self.activity_zip.write(os.path.join(dirname, filename), filename) - self.activity_zip.close() + if title not in self.activity_zip.namelist(): + self.activity_zip.write(ds_objects[i].get_file_path(), title) + self.activity_zip.close() self.ls_right.set_sort_column_id(COLUMN_IMAGE, Gtk.SortType.ASCENDING) def reload_journal_table(self): @@ -1177,7 +1154,7 @@ def _load_document(self, file_path): def write_file(self, file_path): "Save meta data for the file." - if not os.path.exists(self.activity_zip): + if not os.path.exists(self.activity_zip.filename): zf = zipfile.ZipFile(self.activity_zip, 'w') zf.writestr("filler.txt", "filler") zf.close() @@ -1200,8 +1177,8 @@ def write_file(self, file_path): self.annotations.save() self.final_rewrite_zip() os.link( - os.path.abspath(self.activity_zip), file_path) - os.unlink(os.path.abspath(self.activity_zip)) + os.path.abspath(self.activity_zip.filename), file_path) + os.unlink(os.path.abspath(self.activity_zip.filename)) os.remove(self.pickle_file_temp) self.activity_zip = None self.pickle_file_temp = None From 94c9e12dcdb835a16af9bc766f15d963b42964ee Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Fri, 18 Dec 2020 08:16:53 +0100 Subject: [PATCH 4/8] Clean up code Some things that were done are no longer needed, this commit removes them. There's no need to extract any file anymore as they're all journal objects already. Remove the redundant step when looping over journal objects. Signed-off-by: Ibiam Chihurumnaya --- readtoolbar.py | 13 ------------- viewslides.py | 52 +------------------------------------------------- 2 files changed, 1 insertion(+), 64 deletions(-) diff --git a/readtoolbar.py b/readtoolbar.py index 2e05089..c459ede 100644 --- a/readtoolbar.py +++ b/readtoolbar.py @@ -282,13 +282,6 @@ def __init__(self): self._remove_image.props.sensitive = False self._remove_image.show() - self.extract_image = ToolButton('gnome-mime-image') - self.extract_image.set_tooltip(_('Extract Image')) - self.extract_image.connect('clicked', self.extract_image_cb) - self.insert(self.extract_image, -1) - self.extract_image.props.sensitive = False - self.extract_image.show() - def set_activity(self, activity): self.activity = activity @@ -310,12 +303,6 @@ def _remove_image_cb(self, button): if self.activity.get_shared(): self.activity.collab.post(dict(action="remove-image")) - def extract_image_cb(self, button): - self.activity.extract_image() - - if self.activity.get_shared(): - self.activity.collab.post(dict(action="extract")) - def _show_image_tables_cb(self, button): self._hide_image_tables.props.sensitive = True self._reload_journal_table.props.sensitive = True diff --git a/viewslides.py b/viewslides.py index 0000a24..2666aff 100644 --- a/viewslides.py +++ b/viewslides.py @@ -271,7 +271,6 @@ def __init__(self, handle): 'viewslides-files') self.load_journal_table() - self.show_image("ViewSlides.jpg") self.page = 0 self.temp_filename = '' self.saved_screen_width = 0 @@ -539,7 +538,7 @@ def load_journal_table(self): 'image/tiff', 'image/png']}, properties=['uid', 'title', 'mime_type']) self.ls_right.clear() self.activity_zip = zipfile.ZipFile(self.activity_zip, 'w') - for i in range(0, num_objects, 1): + for i in range(0, num_objects): iter = self.ls_right.append() title = ds_objects[i].metadata['title'] mime_type = ds_objects[i].metadata['mime_type'] @@ -606,7 +605,6 @@ def show_image_tables(self, state): self.hpane.hide() self.annotation_textview.show() self.sidebar.show() - self.rewrite_zip() self.set_current_page(0) self._load_document(os.path.abspath(self.activity_zip)) @@ -616,15 +614,6 @@ def selection_left_cb(self, selection): self.selection_left = selection.get_selected() if self.selection_left: model, iter = self.selection_left - selected_file = model.get_value(iter, COLUMN_OLD_NAME) - zf = zipfile.ZipFile(self.activity_zip, 'r') - if self.save_extracted_file(zf, selected_file): - fname = os.path.join( - self.get_activity_root(), - 'instance', - self.make_new_filename(selected_file)) - self.show_image(fname) - os.remove(fname) self._slides_toolbar._remove_image.props.sensitive = True self._slides_toolbar.extract_image.props.sensitive = True @@ -671,19 +660,6 @@ def remove_image(self): self._slides_toolbar._remove_image.props.sensitive = True self.is_dirty = True - def extract_image(self): - if self.selection_left: - model, iter = self.selection_left - selected_file = model.get_value(iter, COLUMN_OLD_NAME) - zf = zipfile.ZipFile(self.activity_zip, 'r') - if self.save_extracted_file(zf, selected_file): - fname = os.path.join( - self.get_activity_root(), - 'instance', - self.make_new_filename(selected_file)) - self.create_journal_entry(fname, selected_file) - os.remove(fname) - def create_journal_entry(self, tempfile, title): journal_entry = datastore.create() journal_entry.metadata['title'] = title @@ -713,30 +689,6 @@ def check_for_duplicates(self, filename): return True return False - def rewrite_zip(self): - if not self.is_dirty: - return - new_zipfile = os.path.join(self.get_activity_root(), 'instance', - 'activity_zip_rewrite') - _logger.debug(self.activity_zip, new_zipfile) - zf_new = zipfile.ZipFile(new_zipfile, 'w') - zf_old = zipfile.ZipFile(self.activity_zip, 'r') - for row in self.ls_left: - copied_file = row[COLUMN_OLD_NAME] - new_file = row[COLUMN_IMAGE] - if self.save_extracted_file(zf_old, copied_file): - outfn = self.make_new_filename(copied_file) - fname = os.path.join( - self.get_activity_root(), 'instance', outfn) - zf_new.write(fname.encode("utf-8"), new_file.encode("utf-8")) - print('rewriting', new_file) - os.remove(fname) - zf_old.close() - zf_new.close() - os.remove(self.activity_zip) - self.activity_zip = new_zipfile - self.is_dirty = False - def final_rewrite_zip(self): if not self.annotations_dirty: return @@ -1231,8 +1183,6 @@ def _message_cb(self, collab, buddy, message): self.add_image() if action == 'remove-image': self.remove_image() - if action == 'extract': - self.extract_image() if action == 'reload': self.reload_journal_table() From 45604a426d2f5ad1433d02ef50b3714860593231 Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Fri, 22 Jan 2021 17:27:51 +0100 Subject: [PATCH 5/8] Fix g_value_get_int: assertion When activity is resumed this error is seen in the logs; Warning: g_value_get_int: assertion 'G_VALUE_HOLDS_INT (value)' failed This commit fixes that as it basically blocks the callbacks to the insert-text and activate signals, then sets the text in self.num_page_entry before unblocking both signals. - Pass the path of self.activity_zip to _load_document - Remove sensitive set on self._slides_toolbar.extract_image as the method was removed earlier and no longer exists. Signed-off-by: Ibiam Chihurumnaya --- viewslides.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/viewslides.py b/viewslides.py index 2666aff..39f8a95 100644 --- a/viewslides.py +++ b/viewslides.py @@ -508,7 +508,16 @@ def update_nav_buttons(self): self.forward.props.sensitive = \ current_page < self.total_pages - 1 + self.num_page_entry.handler_block_by_func( + self.__new_num_page_entry_insert_text_cb) + self.num_page_entry.handler_block_by_func( + self.__new_num_page_entry_activate_cb) self.num_page_entry.props.text = str(current_page + 1) + self.num_page_entry.handler_unblock_by_func( + self.__new_num_page_entry_insert_text_cb) + self.num_page_entry.handler_unblock_by_func( + self.__new_num_page_entry_activate_cb) + self.total_page_label.props.label = \ ' / ' + str(self.total_pages) @@ -615,7 +624,6 @@ def selection_left_cb(self, selection): if self.selection_left: model, iter = self.selection_left self._slides_toolbar._remove_image.props.sensitive = True - self._slides_toolbar.extract_image.props.sensitive = True def selection_right_cb(self, selection): tv = selection.get_tree_view() @@ -1033,7 +1041,7 @@ def extract_pickle_file(self): def read_file(self, file_path): """Load a file from the datastore on activity start""" self.get_saved_page_number() - self._load_document(self.activity_zip) + self._load_document(os.path.abspath(self.activity_zip.filename)) def __delete_event_cb(self, widget, event): os.remove(self.temp_filename) From 752411867366a77adb998acd30cbc507d6c9e628 Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Mon, 26 Apr 2021 03:22:14 +0100 Subject: [PATCH 6/8] Fix fail to start, XML parse error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Ubuntu 20.04 with librsvg 2.48.0 the activity does fail to start and logs contain; Traceback (most recent call last): File "/usr/bin/sugar-activity3", line 5, in activityinstance.main() File "/usr/lib/python3/dist-packages/sugar3/activity/activityinstance.py", line 230, in main instance = create_activity_instance(activity_constructor, activity_handle) File "/usr/lib/python3/dist-packages/sugar3/activity/activityinstance.py", line 59, in create_activity_instance activity = constructor(handle) File "/usr/share/sugar/activities/ViewSlides.activity/viewslides.py", line 160, in __init__ activity.Activity.__init__(self, handle) File "/usr/lib/python3/dist-packages/sugar3/activity/activity.py", line 468, in __init__ self.set_icon_from_file(bundle.get_icon()) gi.repository.GLib.Error: rsvg-error-quark: Failed to load image “/usr/share/sugar/activities/ViewSlides.activity/activity/ViewSlides.svg”: XML parse error: error code=201 (3) in (null):28:45: Namespace prefix sodipodi for ry on path is not defined (0) librsvg 2.47.0 introduced stricter namespaces in the XML parser, https://gitlab.gnome.org/GNOME/librsvg/-/blob/master/NEWS#L67 so add namespace declarations. Signed-off-by: Ibiam Chihurumnaya --- activity/ViewSlides.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activity/ViewSlides.svg b/activity/ViewSlides.svg index 2cbeb5d..a8490f2 100644 --- a/activity/ViewSlides.svg +++ b/activity/ViewSlides.svg @@ -1,7 +1,7 @@ -]> +]> Date: Mon, 26 Apr 2021 03:25:10 +0100 Subject: [PATCH 7/8] Update collabwrapper Signed-off-by: Ibiam Chihurumnaya --- collabwrapper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/collabwrapper.py b/collabwrapper.py index 9b01edd..04736df 100644 --- a/collabwrapper.py +++ b/collabwrapper.py @@ -558,7 +558,7 @@ def __init__(self, connection, object_path, props): def accept_to_file(self, destination_path): ''' Accept the file transfer and write it to a new file. The file must - already exist. + not already exist. Args: destination_path (str): the path where a new file will be @@ -626,10 +626,6 @@ def __splice_done_cb(self, output_stream, res, user): def output(self): return self._destination_path or self._output_stream - @GObject.Property - def socket_address(self): - return self._socket_address - class _BaseOutgoingTransfer(_BaseFileTransfer): ''' From 607794772b69e254155642f677827dd83772912a Mon Sep 17 00:00:00 2001 From: Ibiam Chihurumnaya Date: Tue, 4 May 2021 11:38:36 +0100 Subject: [PATCH 8/8] Change use of iter to avoid clash with builtin iter Use context manager Signed-off-by: Ibiam Chihurumnaya --- viewslides.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/viewslides.py b/viewslides.py index 39f8a95..992b725 100644 --- a/viewslides.py +++ b/viewslides.py @@ -548,7 +548,7 @@ def load_journal_table(self): self.ls_right.clear() self.activity_zip = zipfile.ZipFile(self.activity_zip, 'w') for i in range(0, num_objects): - iter = self.ls_right.append() + selected_iter = self.ls_right.append() title = ds_objects[i].metadata['title'] mime_type = ds_objects[i].metadata['mime_type'] if mime_type == 'image/jpeg' and not title.endswith('.jpg') and not title.endswith( @@ -563,10 +563,10 @@ def load_journal_table(self): if mime_type == 'image/tiff' and not title.endswith( '.tiff') and not title.endswith('.TIFF'): title = title + '.tiff' - self.ls_right.set(iter, COLUMN_IMAGE, title) + self.ls_right.set(selected_iter, COLUMN_IMAGE, title) jobject_wrapper = JobjectWrapper() jobject_wrapper.set_jobject(ds_objects[i]) - self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) + self.ls_right.set(selected_iter, COLUMN_PATH, jobject_wrapper) if title not in self.activity_zip.namelist(): self.activity_zip.write(ds_objects[i].get_file_path(), title) @@ -578,12 +578,12 @@ def reload_journal_table(self): if os.path.exists(self.activity_zip.filename) and zipfile.is_zipfile(self.activity_zip): zf = zipfile.ZipFile(self.activity_zip, 'r') for filename in zf.namelist(): - iter = self.ls_right.append() + selected_iter = self.ls_right.append() jobject_wrapper = JobjectWrapper() jobject_wrapper.set_file_path( os.path.abspath(self.activity_zip.filename)) - self.ls_right.set(iter, COLUMN_IMAGE, filename) - self.ls_right.set(iter, COLUMN_PATH, jobject_wrapper) + self.ls_right.set(selected_iter, COLUMN_IMAGE, filename) + self.ls_right.set(selected_iter, COLUMN_PATH, jobject_wrapper) else: self.load_journal_table() @@ -622,7 +622,7 @@ def selection_left_cb(self, selection): model = tv.get_model() self.selection_left = selection.get_selected() if self.selection_left: - model, iter = self.selection_left + model, selected_iter = self.selection_left self._slides_toolbar._remove_image.props.sensitive = True def selection_right_cb(self, selection): @@ -630,13 +630,13 @@ def selection_right_cb(self, selection): model = tv.get_model() sel = selection.get_selected() if sel: - model, iter = sel - jobject = model.get_value(iter, COLUMN_PATH) + model, selected_iter = sel + jobject = model.get_value(selected_iter, COLUMN_PATH) fname = jobject.get_file_path() self.show_image(fname) self._slides_toolbar._add_image.props.sensitive = True self.selected_journal_entry = jobject - self.selected_title = model.get_value(iter, COLUMN_IMAGE) + self.selected_title = model.get_value(selected_iter, COLUMN_IMAGE) def add_image(self): if self.selected_journal_entry is None: @@ -651,9 +651,9 @@ def add_image(self): ' already exists in slideshow!') return - iter = self.ls_left.append() + selected_iter = self.ls_left.append() self.ls_left.set( - iter, + selected_iter, COLUMN_IMAGE, arcname, COLUMN_OLD_NAME, @@ -663,8 +663,8 @@ def add_image(self): def remove_image(self): if self.selection_left: - model, iter = self.selection_left - self.ls_left.remove(iter) + model, selected_iter = self.selection_left + self.ls_left.remove(selected_iter) self._slides_toolbar._remove_image.props.sensitive = True self.is_dirty = True @@ -1017,11 +1017,9 @@ def save_extracted_file(self, zipfile, filename): if (outfn == ''): return False fname = os.path.join(self.get_activity_root(), 'instance', outfn) - f = open(fname, 'wb') - try: + with open(fname, 'wb') as f: f.write(filebytes) - finally: - f.close() + return True def extract_pickle_file(self): @@ -1091,9 +1089,9 @@ def _load_document(self, file_path): i = 0 while i < len(self.image_files): newfn = self.make_new_filename(self.image_files[i]) - iter = self.ls_left.append() + selected_iter = self.ls_left.append() self.ls_left.set( - iter, + selected_iter, COLUMN_IMAGE, self.image_files[i], COLUMN_OLD_NAME,