From 8b508bb6de9faf8ce6c65a0499aa04ffc8f91e73 Mon Sep 17 00:00:00 2001
From: Marvin W <git@larma.de>
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()));
             }