Commit a2bbe5f7 authored by Philipp Hörist's avatar Philipp Hörist
Browse files

HTTPUpload: Refactor code

- Refactor File class to a more generic FileTransfer class

- FileTransfer class holds the transfer state and notifies other objects via signals
parent 77ed800a
......@@ -829,3 +829,36 @@ GIO_TLS_ERRORS = {
Gio.TlsCertificateFlags.EXPIRED: _('The certificate has expired'),
}
# pylint: enable=line-too-long
class FTState(Enum):
PREPARING = 'prepare'
ENCRYPTING = 'encrypt'
STARTED = 'started'
IN_PROGRESS = 'progress'
FINISHED = 'finished'
ERROR = 'error'
@property
def is_preparing(self):
return self == FTState.PREPARING
@property
def is_encrypting(self):
return self == FTState.ENCRYPTING
@property
def is_started(self):
return self == FTState.STARTED
@property
def is_in_progress(self):
return self == FTState.IN_PROGRESS
@property
def is_finished(self):
return self == FTState.FINISHED
@property
def is_error(self):
return self == FTState.ERROR
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
from gajim.common.helpers import Observable
from gajim.common.const import FTState
class FileTransfer(Observable):
def __init__(self, account):
Observable.__init__(self)
self._account = account
self._seen = 0
self.size = 0
self._state = None
self._error_text = ''
@property
def account(self):
return self._account
@property
def seen(self):
return self._seen
@property
def is_complete(self):
if self.size == 0:
return False
return self._seen >= self.size
def set_preparing(self):
self._state = FTState.PREPARING
self.notify('state-changed', FTState.PREPARING)
def set_encrypting(self):
self._state = FTState.ENCRYPTING
self.notify('state-changed', FTState.ENCRYPTING)
def set_started(self):
self._state = FTState.STARTED
self.notify('state-changed', FTState.STARTED)
def set_error(self, text=''):
self._error_text = text
self._state = FTState.ERROR
self.notify('state-changed', FTState.ERROR)
self.disconnect_signals()
def set_in_progress(self):
self._state = FTState.IN_PROGRESS
self.notify('state-changed', FTState.IN_PROGRESS)
def set_finished(self):
self._state = FTState.FINISHED
self.notify('state-changed', FTState.FINISHED)
self.disconnect_signals()
def update_progress(self):
self.notify('progress')
......@@ -44,7 +44,9 @@ import logging
import json
import shutil
import collections
from collections import defaultdict
import random
import weakref
import string
from string import Template
import urllib
......@@ -1705,3 +1707,34 @@ def get_tls_error_phrase(tls_error):
if phrase is None:
return GIO_TLS_ERRORS.get(Gio.TlsCertificateFlags.GENERIC_ERROR)
return phrase
class Observable:
def __init__(self, log_=None):
self._log = log_
self._callbacks = defaultdict(list)
def disconnect_signals(self):
self._callbacks = defaultdict(list)
def disconnect(self, object_):
for signal_name, handlers in self._callbacks.items():
for handler in list(handlers):
func = handler()
if func is None or func.__self__ == object_:
self._callbacks[signal_name].remove(handler)
def connect(self, signal_name, func):
weak_func = weakref.WeakMethod(func)
self._callbacks[signal_name].append(weak_func)
def notify(self, signal_name, *args, **kwargs):
if self._log is not None:
self._log.info('Signal: %s', signal_name)
callbacks = self._callbacks.get(signal_name, [])
for func in list(callbacks):
if func() is None:
self._callbacks[signal_name].remove(func)
continue
func()(self, signal_name, *args, **kwargs)
......@@ -28,8 +28,8 @@ from gi.repository import Soup
from gajim.common import app
from gajim.common import ged
from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.helpers import get_tls_error_phrase
from gajim.common.filetransfer import FileTransfer
from gajim.common.modules.base import BaseModule
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import MessageOutgoingEvent
......@@ -143,47 +143,44 @@ class HTTPUpload(BaseModule):
self._log.info("Detected MIME type of file: %s", mime)
try:
file = File(path,
contact,
self._account,
mime,
encryption,
groupchat)
app.interface.show_httpupload_progress(file)
transfer = HTTPFileTransfer(path,
contact,
self._account,
mime,
encryption,
groupchat)
app.interface.show_httpupload_progress(transfer)
except Exception as error:
self._log.exception('Error while loading file')
self._raise_information_event('open-file-error2', str(error))
return
if encryption is not None:
app.interface.encrypt_file(file, self._account, self._request_slot)
app.interface.encrypt_file(transfer,
self._account,
self._request_slot)
else:
self._request_slot(file)
self._request_slot(transfer)
def cancel_upload(self, file):
message = self._queued_messages.get(id(file))
def cancel_upload(self, transfer):
message = self._queued_messages.get(id(transfer))
if message is None:
return
self._session.cancel_message(message, Soup.Status.CANCELLED)
@staticmethod
def _raise_progress_event(status, file, seen=None, total=None):
app.nec.push_incoming_event(HTTPUploadProgressEvent(
None, status=status, file=file, seen=seen, total=total))
@staticmethod
def _raise_information_event(dialog_name, args=None):
app.nec.push_incoming_event(InformationEvent(
None, dialog_name=dialog_name, args=args))
def _request_slot(self, file):
GLib.idle_add(self._raise_progress_event, 'request', file)
iq = self._build_request(file)
def _request_slot(self, transfer):
transfer.set_preparing()
iq = self._build_request(transfer)
self._log.info("Sending request for slot")
self._con.connection.SendAndCallForResponse(
iq, self._received_slot, {'file': file})
iq, self._received_slot, {'transfer': transfer})
def _build_request(self, file):
def _build_request(self, transfer):
iq = nbxmpp.Iq(typ='get', to=self.component)
id_ = app.get_an_id()
iq.setID(id_)
......@@ -191,13 +188,14 @@ class HTTPUpload(BaseModule):
# experimental namespace
request = iq.setTag(name="request",
namespace=self.httpupload_namespace)
request.addChild('filename', payload=os.path.basename(file.path))
request.addChild('size', payload=file.size)
request.addChild('content-type', payload=file.mime)
request.addChild('filename',
payload=os.path.basename(transfer.path))
request.addChild('size', payload=transfer.size)
request.addChild('content-type', payload=transfer.mime)
else:
attr = {'filename': os.path.basename(file.path),
'size': file.size,
'content-type': file.mime}
attr = {'filename': os.path.basename(transfer.path),
'size': transfer.size,
'content-type': transfer.mime}
iq.setTag(name="request",
namespace=self.httpupload_namespace,
attrs=attr)
......@@ -215,10 +213,10 @@ class HTTPUpload(BaseModule):
return stanza.getErrorMsg()
def _received_slot(self, _con, stanza, file):
def _received_slot(self, _con, stanza, transfer):
self._log.info("Received slot")
if stanza.getType() == 'error':
self._raise_progress_event('close', file)
transfer.set_error()
self._raise_information_event('request-upload-slot-error',
self._get_slot_error_message(stanza))
self._log.error(stanza)
......@@ -226,12 +224,12 @@ class HTTPUpload(BaseModule):
try:
if self.httpupload_namespace == NS_HTTPUPLOAD:
file.put_uri = stanza.getTag('slot').getTag('put').getData()
file.get_uri = stanza.getTag('slot').getTag('get').getData()
transfer.put_uri = stanza.getTag('slot').getTag('put').getData()
transfer.get_uri = stanza.getTag('slot').getTag('get').getData()
else:
slot = stanza.getTag('slot')
file.put_uri = slot.getTagAttr('put', 'url')
file.get_uri = slot.getTagAttr('get', 'url')
transfer.put_uri = slot.getTagAttr('put', 'url')
transfer.get_uri = slot.getTagAttr('get', 'url')
for header in slot.getTag('put').getTags('header'):
name = header.getAttr('name')
if name not in self._allowed_headers:
......@@ -239,29 +237,29 @@ class HTTPUpload(BaseModule):
data = header.getData()
if '\n' in data:
raise ValueError('Newline in header data')
file.append_header(name, data)
transfer.append_header(name, data)
except Exception:
self._log.error("Got invalid stanza: %s", stanza)
self._log.exception('Error')
self._raise_progress_event('close', file)
transfer.set_error()
self._raise_information_event('request-upload-slot-error2')
return
if (urlparse(file.put_uri).scheme != 'https' or
urlparse(file.get_uri).scheme != 'https'):
self._raise_progress_event('close', file)
if (urlparse(transfer.put_uri).scheme != 'https' or
urlparse(transfer.get_uri).scheme != 'https'):
transfer.set_error()
self._raise_information_event('unsecure-error')
return
self._log.info('Uploading file to %s', file.put_uri)
self._log.info('Please download from %s', file.get_uri)
self._log.info('Uploading file to %s', transfer.put_uri)
self._log.info('Please download from %s', transfer.get_uri)
self._upload_file(file)
self._upload_file(transfer)
def _upload_file(self, file):
self._raise_progress_event('upload', file)
def _upload_file(self, transfer):
transfer.set_started()
message = Soup.Message.new('PUT', file.put_uri)
message = Soup.Message.new('PUT', transfer.put_uri)
message.connect('starting', self._check_certificate)
# Set CAN_REBUILD so chunks get discarded after they are beeing
......@@ -269,16 +267,16 @@ class HTTPUpload(BaseModule):
message.set_flags(Soup.MessageFlags.CAN_REBUILD)
message.props.request_body.set_accumulate(False)
message.props.request_headers.set_content_type(file.mime, None)
message.props.request_headers.set_content_length(file.size)
for name, value in file.headers:
message.props.request_headers.set_content_type(transfer.mime, None)
message.props.request_headers.set_content_length(transfer.size)
for name, value in transfer.headers:
message.props.request_headers.append(name, value)
message.connect('wrote-headers', self._on_wrote_headers, file)
message.connect('wrote-chunk', self._on_wrote_chunk, file)
message.connect('wrote-headers', self._on_wrote_headers, transfer)
message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
self._queued_messages[id(file)] = message
self._session.queue_message(message, self._on_finish, file)
self._queued_messages[id(transfer)] = message
self._session.queue_message(message, self._on_finish, transfer)
def _check_certificate(self, message):
https_used, _tls_certificate, tls_errors = message.get_https_status()
......@@ -299,11 +297,9 @@ class HTTPUpload(BaseModule):
self._raise_information_event('httpupload-error', phrase)
return
def _on_finish(self, _session, message, file):
self._raise_progress_event('close', file)
self._queued_messages.pop(id(file), None)
file.set_finished()
def _on_finish(self, _session, message, transfer):
self._queued_messages.pop(id(transfer), None)
transfer.set_finished()
if message.props.status_code == Soup.Status.CANCELLED:
self._log.info('Upload cancelled')
......@@ -311,21 +307,21 @@ class HTTPUpload(BaseModule):
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
self._log.info('Upload completed successfully')
uri = file.get_transformed_uri()
uri = transfer.get_transformed_uri()
self._text.append(uri)
if file.is_groupchat:
if transfer.is_groupchat:
app.nec.push_outgoing_event(
GcMessageOutgoingEvent(None,
account=self._account,
jid=file.contact.jid,
jid=transfer.contact.jid,
message=uri,
automatic_message=False))
else:
app.nec.push_outgoing_event(
MessageOutgoingEvent(None,
account=self._account,
jid=file.contact.jid,
jid=transfer.contact.jid,
message=uri,
type_='chat',
automatic_message=False))
......@@ -336,13 +332,13 @@ class HTTPUpload(BaseModule):
phrase)
self._raise_information_event('httpupload-response-error', phrase)
def _on_wrote_chunk(self, message, file):
self._raise_progress_event('update', file, file.seen, file.size)
if file.is_complete:
def _on_wrote_chunk(self, message, transfer):
transfer.update_progress()
if transfer.is_complete:
message.props.request_body.complete()
return
bytes_ = file.get_chunk()
bytes_ = transfer.get_chunk()
self._session.pause_message(message)
GLib.idle_add(self._append, message, bytes_)
......@@ -353,11 +349,11 @@ class HTTPUpload(BaseModule):
message.props.request_body.append(bytes_)
@staticmethod
def _on_wrote_headers(message, file):
message.props.request_body.append(file.get_chunk())
def _on_wrote_headers(message, transfer):
message.props.request_body.append(transfer.get_chunk())
class File:
class HTTPFileTransfer(FileTransfer):
def __init__(self,
path,
contact,
......@@ -365,12 +361,12 @@ class File:
mime,
encryption,
groupchat):
FileTransfer.__init__(self, account)
self._path = path
self._encryption = encryption
self._groupchat = groupchat
self._contact = contact
self._account = account
self._mime = mime
self.size = os.stat(path).st_size
......@@ -380,13 +376,8 @@ class File:
self._stream = None
self._data = None
self._seen = 0
self._headers = {}
@property
def account(self):
return self._account
@property
def mime(self):
return self._mime
......@@ -412,26 +403,23 @@ class File:
return self._uri_transform_func(self.get_uri)
return self.get_uri
@property
def seen(self):
return self._seen
@property
def path(self):
return self._path
@property
def is_complete(self):
return self._seen >= self.size
def append_header(self, name, value):
self._headers[name] = value
def set_uri_transform_func(self, func):
self._uri_transform_func = func
def set_error(self, text=''):
self._close()
super().set_error(text)
def set_finished(self):
self._close()
super().set_finished()
def set_encrypted_data(self, data):
self._data = data
......@@ -462,9 +450,5 @@ class File:
return data
class HTTPUploadProgressEvent(NetworkIncomingEvent):
name = 'httpupload-progress'
def get_instance(*args, **kwargs):
return HTTPUpload(*args, **kwargs), 'HTTPUpload'
......@@ -19,7 +19,6 @@ from gi.repository import Gtk
from gi.repository import GLib
from gajim.common import app
from gajim.common import ged
from gajim.common.i18n import _
from gajim.gtk.util import get_builder
......@@ -27,7 +26,7 @@ from gajim.gtk.util import EventHelper
class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
def __init__(self, file):
def __init__(self, transfer):
Gtk.ApplicationWindow.__init__(self)
EventHelper.__init__(self)
self.set_name('HTTPUploadProgressWindow')
......@@ -37,8 +36,10 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self.set_title(_('File Transfer'))
self._destroyed = False
self._con = app.connections[file.account]
self._file = file
self._con = app.connections[transfer.account]
self._transfer = transfer
self._transfer.connect('state-changed', self._on_transfer_state_change)
self._transfer.connect('progress', self._on_transfer_progress)
if app.config.get('use_kib_mib'):
self._units = GLib.FormatSizeFlags.IEC_UNITS
......@@ -48,7 +49,7 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self._start_time = time.time()
self._ui = get_builder('httpupload_progress_dialog.ui')
self._ui.file_name_label.set_text(os.path.basename(file.path))
self._ui.file_name_label.set_text(os.path.basename(transfer.path))
self.add(self._ui.box)
self._pulse = GLib.timeout_add(100, self._pulse_progressbar)
......@@ -57,23 +58,17 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self.connect('destroy', self._on_destroy)
self._ui.connect_signals(self)
self.register_events([
('httpupload-progress', ged.CORE, self._on_httpupload_progress),
])
def _on_httpupload_progress(self, obj):
if self._file != obj.file:
return
if obj.status == 'request':
def _on_transfer_state_change(self, _transfer, _signal_name, state):
if state.is_preparing:
self._ui.label.set_text(_('Requesting HTTP File Upload Slot…'))
elif obj.status == 'close':
elif state.is_finished or state.is_error:
self.destroy()
elif obj.status == 'upload':
elif state.is_started:
self._ui.label.set_text(_('Uploading via HTTP File Upload…'))
elif obj.status == 'update':
self._update_progress(obj.seen, obj.total)
elif obj.status == 'encrypt':
elif state.is_encrypting:
self._ui.label.set_text(_('Encrypting file…'))
def _pulse_progressbar(self):
......@@ -84,13 +79,14 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self.destroy()
def _on_destroy(self, *args):
self._con.get_module('HTTPUpload').cancel_upload(self._file)
self._con.get_module('HTTPUpload').cancel_upload(self._transfer)
self._destroyed = True
self._file = None
self._transfer.disconnect(self)
self._transfer = None
if self._pulse is not None:
GLib.source_remove(self._pulse)
def _update_progress(self, seen, total):
def _on_transfer_progress(self, transfer, _signal_name):
if self._destroyed:
return
if self._pulse is not None:
......@@ -98,17 +94,17 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self._pulse = None
time_now = time.time()
bytes_sec = round(seen / (time_now - self._start_time), 1)
bytes_sec = round(transfer.seen / (time_now - self._start_time), 1)
size_progress = GLib.format_size_full(seen, self._units)
size_total = GLib.format_size_full(total, self._units)
size_progress = GLib.format_size_full(transfer.seen, self._units)
size_total = GLib.format_size_full(transfer.size, self._units)
speed = '%s/s' % GLib.format_size_full(bytes_sec, self._units)
if bytes_sec == 0:
eta = '∞'
else:
eta = self._format_eta(
round((total - seen) / bytes_sec))
round((transfer.size - transfer.seen) / bytes_sec))
self._ui.progress_label.set_text(
_('%(progress)s of %(total)s') % {
......@@ -117,7 +113,7 @@ class HTTPUploadProgressWindow(Gtk.ApplicationWindow, EventHelper):
self._ui.speed_label.set_text(speed)
self._ui.eta_label.set_text(eta)
self._ui.progressbar.set_fraction(float(seen) / total)
self._ui.progressbar.set_fraction(float(transfer.seen) / transfer.size)