From d1e42a72a00fa224e8d5ea4d61164a0283521282 Mon Sep 17 00:00:00 2001 From: miruka Date: Sun, 8 Mar 2020 05:24:07 -0400 Subject: [PATCH] Fix upload pause/cancel --- TODO.md | 1 - src/backend/matrix_client.py | 50 ++++++++++++++++++-- src/backend/models/items.py | 10 ++-- src/gui/Pages/Chat/FileTransfer/Transfer.qml | 14 +++--- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 8cbec04c..7d9f1ad0 100644 --- a/TODO.md +++ b/TODO.md @@ -50,7 +50,6 @@ by switching to another room and coming back - First sent message in E2E room is sometimes undecryptable -- Pause upload, switch to other room, then come back → wrong state displayed - Pausing uploads doesn't work well, servers end up dropping the connection - In the "Leave me" room, "join > Hi > left" aren't combined diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 3ce2a79d..c3828b12 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -112,8 +112,11 @@ class MatrixClient(nio.AsyncClient): self.profile_task: Optional[asyncio.Future] = None self.sync_task: Optional[asyncio.Future] = None self.load_rooms_task: Optional[asyncio.Future] = None - self.first_sync_done: asyncio.Event = asyncio.Event() - self.first_sync_date: Optional[datetime] = None + + self.upload_monitors: Dict[UUID, nio.TransferMonitor] = {} + + self.first_sync_done: asyncio.Event = asyncio.Event() + self.first_sync_date: Optional[datetime] = None self.past_tokens: Dict[str, str] = {} # {room_id: token} self.fully_loaded_rooms: Set[str] = set() # {room_id} @@ -276,6 +279,25 @@ class MatrixClient(nio.AsyncClient): await self._send_message(room_id, content) + async def toggle_pause_upload( + self, room_id: str, uuid: Union[str, UUID], + ) -> None: + if isinstance(uuid, str): + uuid = UUID(uuid) + + pause = not self.upload_monitors[uuid].pause + + self.upload_monitors[uuid].pause = pause + self.models[room_id, "uploads"][str(uuid)].paused = pause + + + async def cancel_upload(self, uuid: Union[str, UUID]) -> None: + if isinstance(uuid, str): + uuid = UUID(uuid) + + self.upload_monitors[uuid].cancel = True + + async def send_file(self, room_id: str, path: Union[Path, str]) -> None: """Send a `m.file`, `m.image`, `m.audio` or `m.video` message.""" @@ -285,6 +307,7 @@ class MatrixClient(nio.AsyncClient): await self._send_file(item_uuid, room_id, path) except (nio.TransferCancelledError, asyncio.CancelledError): log.info("Deleting item for cancelled upload %s", item_uuid) + del self.upload_monitors[item_uuid] del self.models[room_id, "uploads"][str(item_uuid)] @@ -306,9 +329,10 @@ class MatrixClient(nio.AsyncClient): # This error will be caught again by the try block later below size = 0 - task = asyncio.Task.current_task() monitor = nio.TransferMonitor(size) - upload_item = Upload(item_uuid, task, monitor, path, total_size=size) + upload_item = Upload(item_uuid, path, total_size=size) + + self.upload_monitors[item_uuid] = monitor self.models[room_id, "uploads"][str(item_uuid)] = upload_item def on_transferred(transferred: int) -> None: @@ -325,8 +349,14 @@ class MatrixClient(nio.AsyncClient): url, mime, crypt_dict = await self.upload( lambda *_: path, filename = path.name, - encrypt = encrypt, monitor=monitor, + encrypt = encrypt, + monitor = monitor, ) + + # FIXME: nio might not catch the cancel in time + if monitor.cancel: + raise nio.TransferCancelledError() + except (MatrixError, OSError) as err: upload_item.status = UploadStatus.Error upload_item.error = type(err) @@ -388,12 +418,21 @@ class MatrixClient(nio.AsyncClient): upload_item.total_size = len(thumb_data) try: + # The total_size passed to the monitor only considers + # the file itself, and not the thumbnail. + monitor.on_transferred = None + thumb_url, _, thumb_crypt_dict = await self.upload( lambda *_: thumb_data, filename = f"{path.stem}_sample{''.join(path.suffixes)}", encrypt = encrypt, + monitor = monitor, ) + + # FIXME: nio might not catch the cancel in time + if monitor.cancel: + raise nio.TransferCancelledError() except MatrixError as err: log.warning(f"Failed uploading thumbnail {path}: {err}") else: @@ -451,6 +490,7 @@ class MatrixClient(nio.AsyncClient): content["msgtype"] = "m.file" content["filename"] = path.name + del self.upload_monitors[item_uuid] del self.models[room_id, "uploads"][str(upload_item.id)] await self._local_echo( diff --git a/src/backend/models/items.py b/src/backend/models/items.py index 992bad0e..6c189a0b 100644 --- a/src/backend/models/items.py +++ b/src/backend/models/items.py @@ -2,7 +2,6 @@ """`ModelItem` subclasses definitions.""" -import asyncio import json from dataclasses import dataclass, field from datetime import datetime, timedelta @@ -136,18 +135,17 @@ class UploadStatus(AutoStrEnum): @dataclass -class Upload(ModelItem): # XXX +class Upload(ModelItem): """Represent a running or failed file upload operation.""" - id: UUID = field() - task: asyncio.Task = field() - monitor: nio.TransferMonitor = field() - filepath: Path = field() + id: UUID = field() + filepath: Path = field() total_size: int = 0 uploaded: int = 0 speed: float = 0 time_left: timedelta = timedelta(0) + paused: bool = False status: UploadStatus = UploadStatus.Uploading error: OptionalExceptionType = type(None) diff --git a/src/gui/Pages/Chat/FileTransfer/Transfer.qml b/src/gui/Pages/Chat/FileTransfer/Transfer.qml index 9cd64e02..44241f7a 100644 --- a/src/gui/Pages/Chat/FileTransfer/Transfer.qml +++ b/src/gui/Pages/Chat/FileTransfer/Transfer.qml @@ -9,13 +9,12 @@ HColumnLayout { id: transfer - property bool paused: false - property int msLeft: model.time_left || 0 property int uploaded: model.uploaded readonly property int speed: model.speed readonly property int totalSize: model.total_size readonly property string status: model.status + readonly property bool paused: model.paused function cancel() { @@ -23,12 +22,13 @@ HColumnLayout { // immediate visual feedback transfer.height = 0 // Python will delete this model item on cancel - py.call(py.getattr(model.task, "cancel")) + py.callClientCoro(chat.userId, "cancel_upload", [model.id]) } - function pause() { - transfer.paused = ! transfer.paused - py.setattr(model.monitor, "pause", transfer.paused) + function toggle_pause() { + py.callClientCoro( + chat.userId, "toggle_pause_upload", [chat.roomId, model.id], + ) } @@ -139,7 +139,7 @@ HColumnLayout { toolTip.text: transfer.paused ? qsTr("Resume") : qsTr("Pause") - onClicked: transfer.pause() + onClicked: transfer.toggle_pause() Layout.preferredWidth: status === "Uploading" ?