From 193bf38a790b2a124493c3b7ad591f826e0f773d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 30 Mar 2022 10:36:52 -0600 Subject: [PATCH] Allow cancellation of file transfers --- libdino/src/entity/file_transfer.vala | 1 + libdino/src/service/file_manager.vala | 11 +++-- .../file_default_widget.vala | 16 +++++- .../file_widget.vala | 5 ++ plugins/http-files/src/file_provider.vala | 49 +++++++++++++++++-- plugins/http-files/src/file_sender.vala | 3 +- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index 1823478f..20bc1a7a 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -70,6 +70,7 @@ public class FileTransfer : Object { public State state { get; set; default=State.NOT_STARTED; } public int provider { get; set; } public string info { get; set; } + public Cancellable cancellable { get; default=new Cancellable(); } private Database? db; private string storage_dir; diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index a478695c..b82e0afb 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -246,7 +246,7 @@ public class FileManager : StreamInteractionModule, Object { File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); - yield os.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + yield os.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET, Priority.LOW, file_transfer.cancellable); file_transfer.path = file.get_basename(); file_transfer.input_stream = yield file.read_async(); @@ -292,14 +292,15 @@ public class FileManager : StreamInteractionModule, Object { if (is_sender_trustworthy(file_transfer, conversation)) { try { yield get_file_meta(file_provider, file_transfer, conversation, receive_data); - - if (file_transfer.size >= 0 && file_transfer.size < 5000000) { - yield download_file_internal(file_provider, file_transfer, conversation); - } } catch (Error e) { warning("Error downloading file: %s", e.message); file_transfer.state = FileTransfer.State.FAILED; } + if (file_transfer.size >= 0 && file_transfer.size < 5000000) { + download_file_internal.begin(file_provider, file_transfer, conversation, (_, res) => { + download_file_internal.end(res); + }); + } } conversation.last_active = file_transfer.time; diff --git a/main/src/ui/conversation_content_view/file_default_widget.vala b/main/src/ui/conversation_content_view/file_default_widget.vala index 28b7d477..638dab15 100644 --- a/main/src/ui/conversation_content_view/file_default_widget.vala +++ b/main/src/ui/conversation_content_view/file_default_widget.vala @@ -19,6 +19,7 @@ public class FileDefaultWidget : EventBox { public ModelButton file_open_button; public ModelButton file_save_button; + public ModelButton cancel_button; private FileTransfer.State state; @@ -27,6 +28,7 @@ public class FileDefaultWidget : EventBox { this.leave_notify_event.connect(on_pointer_left_event); file_open_button = new ModelButton() { text=_("Open"), visible=true }; file_save_button = new ModelButton() { text=_("Save as…"), visible=true }; + cancel_button = new ModelButton() { text=_("Cancel"), visible=true }; } public void update_file_info(string? mime_type, FileTransfer.State state, long size) { @@ -59,6 +61,18 @@ public class FileDefaultWidget : EventBox { mime_label.label = _("Downloading %s…").printf(get_size_string(size)); spinner.active = true; image_stack.set_visible_child_name("spinner"); + + // Create a menu + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu(); + Box file_menu_box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true }; + file_menu_box.add(cancel_button); + popover_menu.add(file_menu_box); + file_menu.popover = popover_menu; + file_menu.button_release_event.connect(() => { + popover_menu.visible = true; + return true; + }); + popover_menu.closed.connect(on_pointer_left); break; case FileTransfer.State.NOT_STARTED: if (mime_description != null) { @@ -84,7 +98,7 @@ public class FileDefaultWidget : EventBox { if (state == FileTransfer.State.NOT_STARTED) { image_stack.set_visible_child_name("download_image"); } - if (state == FileTransfer.State.COMPLETE) { + if (state == FileTransfer.State.COMPLETE || state == FileTransfer.State.IN_PROGRESS) { file_menu.opacity = 1; } return false; diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 0040db3c..b63195dc 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -131,6 +131,7 @@ public class FileDefaultWidgetController : Object { widget.button_release_event.connect(on_clicked); widget.file_open_button.clicked.connect(open_file); widget.file_save_button.clicked.connect(save_file); + widget.cancel_button.clicked.connect(cancel_download); } public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) { @@ -186,6 +187,10 @@ public class FileDefaultWidgetController : Object { } } + private void cancel_download() { + file_transfer.cancellable.cancel(); + } + private bool on_clicked(EventButton event_button) { switch (state) { case FileTransfer.State.COMPLETE: diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala index e3382439..11885721 100644 --- a/plugins/http-files/src/file_provider.vala +++ b/plugins/http-files/src/file_provider.vala @@ -46,6 +46,38 @@ public class FileProvider : Dino.FileProvider, Object { } } + private class LimitInputStream : InputStream { + InputStream inner; + int64 remaining_size; + + public LimitInputStream(InputStream inner, int64 max_size) { + this.inner = inner; + this.remaining_size = max_size; + } + + private ssize_t check_limit(ssize_t read) throws IOError { + this.remaining_size -= read; + if (remaining_size < 0) throw new IOError.FAILED("Stream length exceeded limit"); + return read; + } + + public override ssize_t read(uint8[] buffer, Cancellable? cancellable = null) throws IOError { + return check_limit(inner.read(buffer, cancellable)); + } + + public override async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + return check_limit(yield inner.read_async(buffer, io_priority, cancellable)); + } + + public override bool close(Cancellable? cancellable = null) throws IOError { + return inner.close(cancellable); + } + + public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + return yield inner.close_async(io_priority, cancellable); + } + } + private void on_file_message(Entities.Message message, Conversation conversation) { var additional_info = message.id.to_string(); @@ -64,9 +96,11 @@ public class FileProvider : Dino.FileProvider, Object { if (http_receive_data == null) return file_meta; var session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; var head_message = new Soup.Message("HEAD", http_receive_data.url); if (head_message != null) { + head_message.request_headers.append("Accept-Encoding", "identity"); try { yield session.send_async(head_message, null); } catch (Error e) { @@ -75,12 +109,12 @@ public class FileProvider : Dino.FileProvider, Object { string? content_type = null, content_length = null; head_message.response_headers.foreach((name, val) => { - if (name == "Content-Type") content_type = val; - if (name == "Content-Length") content_length = val; + if (name.down() == "content-type") content_type = val; + if (name.down() == "content-length") content_length = val; }); file_meta.mime_type = content_type; if (content_length != null) { - file_meta.size = int.parse(content_length); + file_meta.size = int64.parse(content_length); } } @@ -97,9 +131,14 @@ public class FileProvider : Dino.FileProvider, Object { try { var session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; Soup.Request request = session.request(http_receive_data.url); - - return yield request.send_async(null); + InputStream stream = yield request.send_async(file_transfer.cancellable); + if (file_meta.size != -1) { + return new LimitInputStream(stream, file_meta.size); + } else { + return stream; + } } catch (Error e) { throw new FileReceiveError.DOWNLOAD_FAILED("Downloading file error: %s".printf(e.message)); } diff --git a/plugins/http-files/src/file_sender.vala b/plugins/http-files/src/file_sender.vala index e005b8c5..8a22ffe1 100644 --- a/plugins/http-files/src/file_sender.vala +++ b/plugins/http-files/src/file_sender.vala @@ -98,8 +98,9 @@ public class HttpFileSender : FileSender, Object { message.wrote_headers.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body)); message.wrote_chunk.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body)); Soup.Session session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; try { - yield session.send_async(message); + yield session.send_async(message, file_transfer.cancellable); if (message.status_code < 200 || message.status_code >= 300) { throw new FileSendError.UPLOAD_FAILED("HTTP status code %s".printf(message.status_code.to_string())); }