Fix upload pause/cancel

This commit is contained in:
miruka 2020-03-08 05:24:07 -04:00
parent cdb79d11aa
commit d1e42a72a0
4 changed files with 56 additions and 19 deletions

View File

@ -50,7 +50,6 @@
by switching to another room and coming back by switching to another room and coming back
- First sent message in E2E room is sometimes undecryptable - 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 - Pausing uploads doesn't work well, servers end up dropping the connection
- In the "Leave me" room, "join > Hi > left" aren't combined - In the "Leave me" room, "join > Hi > left" aren't combined

View File

@ -112,8 +112,11 @@ class MatrixClient(nio.AsyncClient):
self.profile_task: Optional[asyncio.Future] = None self.profile_task: Optional[asyncio.Future] = None
self.sync_task: Optional[asyncio.Future] = None self.sync_task: Optional[asyncio.Future] = None
self.load_rooms_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.past_tokens: Dict[str, str] = {} # {room_id: token}
self.fully_loaded_rooms: Set[str] = set() # {room_id} self.fully_loaded_rooms: Set[str] = set() # {room_id}
@ -276,6 +279,25 @@ class MatrixClient(nio.AsyncClient):
await self._send_message(room_id, content) 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: 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.""" """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) await self._send_file(item_uuid, room_id, path)
except (nio.TransferCancelledError, asyncio.CancelledError): except (nio.TransferCancelledError, asyncio.CancelledError):
log.info("Deleting item for cancelled upload %s", item_uuid) 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)] 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 # This error will be caught again by the try block later below
size = 0 size = 0
task = asyncio.Task.current_task()
monitor = nio.TransferMonitor(size) 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 self.models[room_id, "uploads"][str(item_uuid)] = upload_item
def on_transferred(transferred: int) -> None: def on_transferred(transferred: int) -> None:
@ -325,8 +349,14 @@ class MatrixClient(nio.AsyncClient):
url, mime, crypt_dict = await self.upload( url, mime, crypt_dict = await self.upload(
lambda *_: path, lambda *_: path,
filename = path.name, 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: except (MatrixError, OSError) as err:
upload_item.status = UploadStatus.Error upload_item.status = UploadStatus.Error
upload_item.error = type(err) upload_item.error = type(err)
@ -388,12 +418,21 @@ class MatrixClient(nio.AsyncClient):
upload_item.total_size = len(thumb_data) upload_item.total_size = len(thumb_data)
try: 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( thumb_url, _, thumb_crypt_dict = await self.upload(
lambda *_: thumb_data, lambda *_: thumb_data,
filename = filename =
f"{path.stem}_sample{''.join(path.suffixes)}", f"{path.stem}_sample{''.join(path.suffixes)}",
encrypt = encrypt, encrypt = encrypt,
monitor = monitor,
) )
# FIXME: nio might not catch the cancel in time
if monitor.cancel:
raise nio.TransferCancelledError()
except MatrixError as err: except MatrixError as err:
log.warning(f"Failed uploading thumbnail {path}: {err}") log.warning(f"Failed uploading thumbnail {path}: {err}")
else: else:
@ -451,6 +490,7 @@ class MatrixClient(nio.AsyncClient):
content["msgtype"] = "m.file" content["msgtype"] = "m.file"
content["filename"] = path.name content["filename"] = path.name
del self.upload_monitors[item_uuid]
del self.models[room_id, "uploads"][str(upload_item.id)] del self.models[room_id, "uploads"][str(upload_item.id)]
await self._local_echo( await self._local_echo(

View File

@ -2,7 +2,6 @@
"""`ModelItem` subclasses definitions.""" """`ModelItem` subclasses definitions."""
import asyncio
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -136,18 +135,17 @@ class UploadStatus(AutoStrEnum):
@dataclass @dataclass
class Upload(ModelItem): # XXX class Upload(ModelItem):
"""Represent a running or failed file upload operation.""" """Represent a running or failed file upload operation."""
id: UUID = field() id: UUID = field()
task: asyncio.Task = field() filepath: Path = field()
monitor: nio.TransferMonitor = field()
filepath: Path = field()
total_size: int = 0 total_size: int = 0
uploaded: int = 0 uploaded: int = 0
speed: float = 0 speed: float = 0
time_left: timedelta = timedelta(0) time_left: timedelta = timedelta(0)
paused: bool = False
status: UploadStatus = UploadStatus.Uploading status: UploadStatus = UploadStatus.Uploading
error: OptionalExceptionType = type(None) error: OptionalExceptionType = type(None)

View File

@ -9,13 +9,12 @@ HColumnLayout {
id: transfer id: transfer
property bool paused: false
property int msLeft: model.time_left || 0 property int msLeft: model.time_left || 0
property int uploaded: model.uploaded property int uploaded: model.uploaded
readonly property int speed: model.speed readonly property int speed: model.speed
readonly property int totalSize: model.total_size readonly property int totalSize: model.total_size
readonly property string status: model.status readonly property string status: model.status
readonly property bool paused: model.paused
function cancel() { function cancel() {
@ -23,12 +22,13 @@ HColumnLayout {
// immediate visual feedback // immediate visual feedback
transfer.height = 0 transfer.height = 0
// Python will delete this model item on cancel // 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() { function toggle_pause() {
transfer.paused = ! transfer.paused py.callClientCoro(
py.setattr(model.monitor, "pause", transfer.paused) chat.userId, "toggle_pause_upload", [chat.roomId, model.id],
)
} }
@ -139,7 +139,7 @@ HColumnLayout {
toolTip.text: transfer.paused ? toolTip.text: transfer.paused ?
qsTr("Resume") : qsTr("Pause") qsTr("Resume") : qsTr("Pause")
onClicked: transfer.pause() onClicked: transfer.toggle_pause()
Layout.preferredWidth: Layout.preferredWidth:
status === "Uploading" ? status === "Uploading" ?