Ticket #2678: file-transfer-progress-v2.patch
File file-transfer-progress-v2.patch, 24.3 KB (added by , 11 months ago) |
---|
-
css/10_header_bar.css
1 /* 2 * This file is part of Xpra. 3 * Copyright (C) 2020 Antoine Martin <antoine@xpra.org> 4 * Xpra is released under the terms of the GNU GPL v2, or, at your option, any 5 * later version. See the file COPYING for details. 6 * 7 * Try to make the header bar use less vertical space 8 * (why is it so hard?) 9 */ 10 11 headerbar entry, 12 headerbar spinbutton, 13 headerbar button, 14 headerbar separator { 15 margin-top: 0px; /* same as headerbar side padding for nicer proportions */ 16 margin-bottom: 0px; 17 } 18 19 headerbar { 20 min-height: 24px; 21 padding-left: 2px; /* same as childrens vertical margins for nicer proportions */ 22 padding-right: 2px; 23 margin: 0px; /* same as headerbar side padding for nicer proportions */ 24 padding: 0px; 25 } -
css/20_progress_bar.css
1 /* 2 * This file is part of Xpra. 3 * Copyright (C) 2020 Antoine Martin <antoine@xpra.org> 4 * Xpra is released under the terms of the GNU GPL v2, or, at your option, any 5 * later version. See the file COPYING for details. 6 * 7 * Make the progress bar more visible 8 * (but we can't set the min-width this way...) 9 */ 10 11 progress, trough { 12 min-height: 30px; 13 } -
setup.py
1682 1682 add_data_files("%s/icons" % share_xpra, glob.glob("icons/*png")) 1683 1683 add_data_files("%s/content-type" % share_xpra, glob.glob("content-type/*")) 1684 1684 add_data_files("%s/content-categories" % share_xpra, glob.glob("content-categories/*")) 1685 add_data_files("%s/css" % share_xpra, glob.glob("./css/*")) 1685 1686 1686 1687 1687 1688 if html5_ENABLED: -
xpra/client/gtk_base/css_overrides.py
1 # This file is part of Xpra. 2 # Copyright (C) 2020 Antoine Martin <antoine@xpra.org> 3 # Xpra is released under the terms of the GNU GPL v2, or, at your option, any 4 # later version. See the file COPYING for details. 5 6 # load xpra's custom css overrides 7 8 import os.path 9 10 from xpra.util import envbool 11 from xpra.platform.paths import get_resources_dir 12 from xpra.log import Logger 13 14 log = Logger("gtk", "util") 15 16 CSS_OVERRIDES = envbool("XPRA_CSS_OVERRIDES", True) 17 18 19 _done = False 20 def inject_css_overrides(): 21 global _done 22 if _done or not CSS_OVERRIDES: 23 return 24 _done = True 25 26 css_dir = os.path.join(get_resources_dir(), "css") 27 log("inject_css_overrides() css_dir=%s", css_dir) 28 from gi.repository import Gtk, Gdk 29 style_provider = Gtk.CssProvider() 30 filename = None 31 def parsing_error(_css_provider, _section, error): 32 log.error("Error: CSS parsing error on") 33 log.error(" '%s'", filename) 34 log.error(" %s", error) 35 style_provider.connect("parsing-error", parsing_error) 36 for f in sorted(os.listdir(css_dir)): 37 filename = os.path.join(css_dir, f) 38 try: 39 style_provider.load_from_path(filename) 40 log(" - loaded '%s'", filename) 41 except Exception as e: 42 log("load_from_path(%s)", filename, exc_info=True) 43 log.error("Error: CSS loading error on") 44 log.error(" '%s'", filename) 45 log.error(" %s", e) 46 47 Gtk.StyleContext.add_provider_for_screen( 48 Gdk.Screen.get_default(), 49 style_provider, 50 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 51 ) -
xpra/client/gtk_base/gtk_client_base.py
35 35 from xpra.client.ui_client_base import UIXpraClient 36 36 from xpra.client.gobject_client_base import GObjectXpraClient 37 37 from xpra.client.gtk_base.gtk_keyboard_helper import GTKKeyboardHelper 38 from xpra.client.gtk_base.css_overrides import inject_css_overrides 38 39 from xpra.client.mixins.window_manager import WindowClient 39 40 from xpra.platform.paths import get_icon_filename 40 41 from xpra.platform.gui import ( … … 66 67 NO_OPENGL_WINDOW_TYPES = os.environ.get("XPRA_NO_OPENGL_WINDOW_TYPES", 67 68 "DOCK,TOOLBAR,MENU,UTILITY,SPLASH,DROPDOWN_MENU,POPUP_MENU,TOOLTIP,NOTIFICATION,COMBO,DND").split(",") 68 69 70 inject_css_overrides() 69 71 72 70 73 class GTKXpraClient(GObjectXpraClient, UIXpraClient): 71 74 __gsignals__ = {} 72 75 #add signals from super classes (all no-arg signals) … … 393 396 #record our response, so we will accept the file 394 397 self.data_send_requests[send_id] = (dtype, url, printit, newopenit) 395 398 cb_answer(accept) 396 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload )399 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload, self.cancel_download) 397 400 self.file_ask_dialog.add_request(rec_answer, send_id, dtype, url, filesize, printit, openit, timeout) 398 401 self.file_ask_dialog.show() 399 402 … … 405 408 406 409 def show_ask_data_dialog(self, *_args): 407 410 from xpra.client.gtk_base.open_requests import getOpenRequestsWindow 408 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload )411 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload, self.cancel_download) 409 412 self.file_ask_dialog.show() 410 413 414 def transfer_progress_update(self, send=True, transfer_id=0, elapsed=0, position=0, total=0): 415 fad = self.file_ask_dialog 416 if fad: 417 self.idle_add(fad.transfer_progress_update, send, transfer_id, elapsed, position, total) 411 418 419 412 420 def accept_data(self, send_id, dtype, url, printit, openit): 413 421 #check if we have accepted this file via the GUI: 414 422 r = self.data_send_requests.pop(send_id, None) -
xpra/client/gtk_base/open_requests.py
10 10 11 11 import gi 12 12 gi.require_version("Gtk", "3.0") 13 gi.require_version("Gdk", "3.0") 13 14 gi.require_version("Pango", "1.0") 14 15 from gi.repository import GLib, Gtk, GdkPixbuf, Pango 15 16 17 from xpra.util import envint 18 from xpra.os_util import monotonic_time, bytestostr, WIN32, OSX 16 19 from xpra.gtk_common.gobject_compat import register_os_signals 17 from xpra.util import envint 18 from xpra.os_util import monotonic_time, bytestostr, get_util_logger, WIN32, OSX 20 from xpra.client.gtk_base.css_overrides import inject_css_overrides 19 21 from xpra.child_reaper import getChildReaper 20 22 from xpra.net.file_transfer import ACCEPT, OPEN, DENY 21 from xpra.simple_stats import std_unit _dec23 from xpra.simple_stats import std_unit, std_unit_dec 22 24 from xpra.gtk_common.gtk_util import ( 23 25 add_close_accel, scaled_image, 24 26 TableBuilder, 25 27 ) 26 28 from xpra.platform.paths import get_icon_dir, get_download_dir 29 from xpra.log import Logger 27 30 28 log = get_util_logger()31 log = Logger("gtk", "file") 29 32 30 33 URI_MAX_WIDTH = envint("XPRA_URI_MAX_WIDTH", 320) 31 34 35 inject_css_overrides() 32 36 37 33 38 _instance = None 34 def getOpenRequestsWindow(show_file_upload_cb=None ):39 def getOpenRequestsWindow(show_file_upload_cb=None, cancel_download=None): 35 40 global _instance 36 41 if _instance is None: 37 _instance = OpenRequestsWindow(show_file_upload_cb )42 _instance = OpenRequestsWindow(show_file_upload_cb, cancel_download) 38 43 return _instance 39 44 40 45 41 46 class OpenRequestsWindow: 42 47 43 def __init__(self, show_file_upload_cb=None ):48 def __init__(self, show_file_upload_cb=None, cancel_download=None): 44 49 self.show_file_upload_cb = show_file_upload_cb 50 self.cancel_download = cancel_download 45 51 self.populate_timer = None 46 52 self.table = None 47 53 self.requests = [] 48 54 self.expire_labels = {} 55 self.progress_bars = {} 49 56 self.window = Gtk.Window() 50 57 self.window.set_border_width(20) 51 58 self.window.connect("delete-event", self.close) … … 106 113 107 114 def update_expires_label(self): 108 115 expired = 0 109 for label, expiry in self.expire_labels. items():116 for label, expiry in self.expire_labels.values(): 110 117 seconds = max(0, expiry-monotonic_time()) 111 118 label.set_text("%i" % seconds) 112 119 if seconds==0: … … 141 148 if dtype==b"file" and filesize>0: 142 149 details = "%sB" % std_unit_dec(filesize) 143 150 expires_label = l() 144 self.expire_labels[ expires_label] = expires151 self.expire_labels[send_id] = (expires_label, expires) 145 152 buttons = self.action_buttons(cb_answer, send_id, dtype, printit, openit) 146 153 s = bytestostr(url) 147 154 main_label = l(s) … … 158 165 self.alignment.add(self.table) 159 166 self.table.show_all() 160 167 168 def remove_entry(self, send_id, can_close=True): 169 self.expire_labels.pop(send_id, None) 170 self.requests = [x for x in self.requests if x[1]!=send_id] 171 self.progress_bars.pop(send_id, None) 172 if not self.requests and can_close: 173 self.close() 174 else: 175 self.populate_table() 176 self.window.resize(1, 1) 177 161 178 def action_buttons(self, cb_answer, send_id, dtype, printit, openit): 162 179 hbox = Gtk.HBox() 163 180 def remove_entry(can_close=False): 164 self.requests = [x for x in self.requests if x[1]!=send_id] 165 if not self.requests and can_close: 166 self.close() 167 else: 168 self.populate_table() 169 self.window.resize(1, 1) 181 self.remove_entry(send_id, can_close) 182 def show_progressbar(): 183 expire = self.expire_labels.pop(send_id, None) 184 if expire: 185 expire_label = expire[0] 186 expire_label.set_text("") 187 for b in hbox.get_children(): 188 hbox.remove(b) 189 def stop(*_args): 190 self.remove_entry(True) 191 self.cancel_download(send_id, "User cancelled") 192 stop_btn = self.btn("Stop", None, stop, "close.png") 193 hbox.pack_start(stop_btn) 194 pb = Gtk.ProgressBar() 195 hbox.set_spacing(20) 196 hbox.pack_start(pb) 197 hbox.show_all() 198 pb.set_size_request(420, 30) 199 self.progress_bars[send_id] = (pb, stop_btn) 200 def cancel(*_args): 201 remove_entry(True) 202 cb_answer(DENY) 170 203 def ok(*_args): 171 204 remove_entry(False) 172 205 cb_answer(ACCEPT, False) 173 def okopen(*_args):174 remove_entry(True)175 cb_answer(ACCEPT, True)176 206 def remote(*_args): 177 207 remove_entry(True) 178 208 cb_answer(OPEN) 179 def cancel(*_args): 180 remove_entry(True) 181 cb_answer(DENY) 182 hbox.pack_start(self.btn("Cancel", None, cancel, "close.png")) 209 def progress(*_args): 210 cb_answer(ACCEPT) 211 show_progressbar() 212 def progressopen(*_args): 213 cb_answer(OPEN) 214 show_progressbar() 215 cancel_btn = self.btn("Cancel", None, cancel, "close.png") 216 hbox.pack_start(cancel_btn) 183 217 if bytestostr(dtype)=="url": 184 218 hbox.pack_start(self.btn("Open Locally", None, ok, "open.png")) 185 219 hbox.pack_start(self.btn("Open on server", None, remote)) 186 220 elif printit: 187 hbox.pack_start(self.btn("Print", None, ok, "printer.png"))221 hbox.pack_start(self.btn("Print", None, progress, "printer.png")) 188 222 else: 189 hbox.pack_start(self.btn("Download", None, ok, "download.png"))223 hbox.pack_start(self.btn("Download", None, progress, "download.png")) 190 224 if openit: 191 hbox.pack_start(self.btn("Download and Open", None, okopen, "open.png"))225 hbox.pack_start(self.btn("Download and Open", None, progressopen, "open.png")) 192 226 hbox.pack_start(self.btn("Open on server", None, remote)) 193 227 return hbox 194 228 … … 203 237 self.populate_timer = 0 204 238 205 239 240 def transfer_progress_update(self, send=True, transfer_id=0, elapsed=0, position=0, total=0): 241 buttons = self.progress_bars.get(transfer_id) 242 if not buttons: 243 #we're not tracking this transfer: no progress bar 244 return 245 pb, stop_btn = buttons 246 log("transfer_progress_update%s pb=%s", (send, transfer_id, elapsed, position, total), pb) 247 if position<0: 248 stop_btn.hide() 249 pb.set_text("Error: file transfer aborted") 250 GLib.timeout_add(5000, self.remove_entry, transfer_id) 251 return 252 if pb: 253 pb.set_fraction(position/total) 254 pb.set_text("%sB of %s" % (std_unit(position), std_unit(total))) 255 pb.set_show_text(True) 256 if position==total: 257 stop_btn.hide() 258 pb.set_text("Complete: %i bytes" % total) 259 pb.set_show_text(True) 260 GLib.timeout_add(5000, self.remove_entry, transfer_id) 261 262 206 263 def show(self): 207 264 log("show()") 208 265 self.window.show_all() … … 235 292 cmd = ["open", downloads] 236 293 else: 237 294 cmd = ["xdg-open", downloads] 238 proc = subprocess.Popen(cmd) 239 getChildReaper().add_process(proc, "show-downloads", cmd, ignore=True, forget=True) 295 try: 296 proc = subprocess.Popen(cmd) 297 except Exception as e: 298 log("show_downloads()", exc_info=True) 299 log.error("Error: failed to open 'Downloads' folder:") 300 log.error(" %s", e) 301 else: 302 getChildReaper().add_process(proc, "show-downloads", cmd, ignore=True, forget=True) 240 303 241 304 242 305 def run(self): -
xpra/net/file_transfer.py
37 37 ACCEPT = 1 38 38 OPEN = 2 39 39 40 def osclose(fd): 41 try: 42 os.close(fd) 43 except OSError as e: 44 filelog("os.close(%s)", fd, exc_info=True) 45 filelog.error("Error closing file download:") 46 filelog.error(" %s", e) 47 40 48 def basename(filename): 41 49 #we can't use os.path.basename, 42 50 #because the remote end may have sent us a filename … … 242 250 } 243 251 return info 244 252 245 def check_digest(self, filename, digest, expected_digest, algo="sha1"):246 if digest!=expected_digest:247 filelog.error("Error: data does not match, invalid %s file digest for '%s'", algo, filename)248 filelog.error(" received %s, expected %s", digest, expected_digest)249 raise Exception("failed %s digest verification" % algo)250 else:251 filelog("%s digest matches: %s", algo, digest)252 253 254 def digest_mismatch(self, filename, digest, expected_digest, algo="sha1"): 255 filelog.error("Error: data does not match, invalid %s file digest for '%s'", algo, filename) 256 filelog.error(" received %s, expected %s", digest, expected_digest) 257 raise Exception("failed %s digest verification" % algo) 253 258 259 254 260 def _check_chunk_receiving(self, chunk_id, chunk_no): 255 261 chunk_state = self.receive_chunks_in_progress.get(chunk_id) 256 262 filelog("_check_chunk_receiving(%s, %s) chunk_state=%s", chunk_id, chunk_no, chunk_state) 257 263 if chunk_state: 264 if chunk_state[-4]: 265 #transfer has been cancelled 266 return 258 267 chunk_state[-2] = 0 #this timer has been used 259 268 if chunk_state[-1]==0: 260 269 filelog.error("Error: chunked file transfer timed out") 261 del self.receive_chunks_in_progress[chunk_id]270 self.receive_chunks_in_progress.pop(chunk_id, None) 262 271 272 def cancel_download(self, send_id, message="Cancelled"): 273 filelog("cancel_download(%s, %s)", send_id, message) 274 for chunk_id, chunk_state in dict(self.receive_chunks_in_progress).items(): 275 if chunk_state[-3]==send_id: 276 self.cancel_file(chunk_id, message) 277 return 278 filelog.error("Error: cannot cancel download %s, entry not found!", bytestostr(send_id)) 279 280 def cancel_file(self, chunk_id, message, chunk=0): 281 filelog("cancel_file%s", (chunk_id, message, chunk)) 282 chunk_state = self.receive_chunks_in_progress.get(chunk_id) 283 if chunk_state: 284 #mark it as cancelled: 285 chunk_state[-4] = True 286 #remove this transfer after a little while, 287 #so in-flight packets won't cause errors 288 def clean_receive_state(): 289 self.receive_chunks_in_progress.pop(chunk_id, None) 290 return False 291 self.timeout_add(20000, clean_receive_state) 292 self.send("ack-file-chunk", chunk_id, False, message, chunk) 293 263 294 def _process_send_file_chunk(self, packet): 264 295 chunk_id, chunk, file_data, has_more = packet[1:5] 265 296 filelog("_process_send_file_chunk%s", (chunk_id, chunk, "%i bytes" % len(file_data), has_more)) … … 266 297 chunk_state = self.receive_chunks_in_progress.get(chunk_id) 267 298 if not chunk_state: 268 299 filelog.error("Error: cannot find the file transfer id '%s'", nonl(bytestostr(chunk_id))) 269 self. send("ack-file-chunk", chunk_id, False, "file transfer id not found", chunk)300 self.cancel_file(chunk_id, "file transfer id %s not found" % chunk_id, chunk) 270 301 return 302 if chunk_state[-4]: 303 filelog("got chunk for a cancelled file transfer, ignoring it") 304 return 305 def progress(position): 306 start = chunk_state[0] 307 send_id = chunk_state[-3] 308 filesize = chunk_state[6] 309 self.transfer_progress_update(False, send_id, monotonic_time()-start, position, filesize) 271 310 fd = chunk_state[1] 272 311 if chunk_state[-1]+1!=chunk: 273 312 filelog.error("Error: chunk number mismatch, expected %i but got %i", chunk_state[-1]+1, chunk) 274 self. send("ack-file-chunk", chunk_id, False, "chunk number mismatch", chunk)275 del self.receive_chunks_in_progress[chunk_id]276 os.close(fd)313 self.cancel_file(chunk_id, "chunk number mismatch", chunk) 314 osclose(fd) 315 progress(-1) 277 316 return 278 317 #update chunk number: 279 318 chunk_state[-1] = chunk … … 287 326 except OSError as e: 288 327 filelog.error("Error: cannot write file chunk") 289 328 filelog.error(" %s", e) 290 self.send("ack-file-chunk", chunk_id, False, "write error: %s" % e, chunk) 291 del self.receive_chunks_in_progress[chunk_id] 292 try: 293 os.close(fd) 294 except OSError: 295 pass 329 self.cancel_file(chunk_id, "write error: %s" % e, chunk) 330 osclose(fd) 331 progress(-1) 296 332 return 297 333 self.send("ack-file-chunk", chunk_id, True, "", chunk) 298 334 if has_more: 335 progress(written) 299 336 timer = chunk_state[-2] 300 337 if timer: 301 338 self.source_remove(timer) … … 303 340 timer = self.timeout_add(CHUNK_TIMEOUT, self._check_chunk_receiving, chunk_id, chunk) 304 341 chunk_state[-2] = timer 305 342 return 306 del self.receive_chunks_in_progress[chunk_id]307 os .close(fd)343 self.receive_chunks_in_progress.pop(chunk_id, None) 344 osclose(fd) 308 345 #check file size and digest then process it: 309 346 filename, mimetype, printit, openit, filesize, options = chunk_state[2:8] 310 347 if written!=filesize: 311 348 filelog.error("Error: expected a file of %i bytes, got %i", filesize, written) 349 progress(-1) 312 350 return 313 351 expected_digest = options.get("sha1") 314 if expected_digest: 315 self.check_digest(filename, digest.hexdigest(), expected_digest) 352 if expected_digest and digest.hexdigest()!=expected_digest: 353 progress(-1) 354 self.digest_mismatch(filename, digest, expected_digest, "sha1") 355 return 356 357 progress(written) 316 358 start_time = chunk_state[0] 317 359 elapsed = monotonic_time()-start_time 318 360 mimetype = bytestostr(mimetype) … … 397 439 monotonic_time(), 398 440 fd, filename, mimetype, 399 441 printit, openit, filesize, 400 options, digest, 0, timer, chunk, 442 options, digest, 0, False, send_id, 443 timer, chunk, 401 444 ] 402 445 self.receive_chunks_in_progress[chunk_id] = chunk_state 403 446 self.send("ack-file-chunk", chunk_id, True, "", chunk) … … 415 458 u = libfn() 416 459 u.update(file_data) 417 460 l("%s digest: %s - expected: %s", algo, u.hexdigest(), digest) 418 self.check_digest(basefilename, u.hexdigest(), digest, algo) 461 if digest!=u.hexdigest(): 462 self.digest_mismatch(filename, digest, u.hexdigest(), algo) 419 463 check_digest("sha1", hashlib.sha1) 420 464 check_digest("md5", hashlib.md5) 421 465 try: … … 790 834 chunk_state = self.send_chunks_in_progress.get(chunk_id) 791 835 filelog("_check_chunk_sending(%s, %s) chunk_state found: %s", chunk_id, chunk_no, bool(chunk_state)) 792 836 if chunk_state: 793 chunk_state[ -2] = 0 #timer has fired837 chunk_state[3] = 0 #timer has fired 794 838 if chunk_state[-1]==chunk_no: 795 839 filelog.error("Error: chunked file transfer timed out on chunk %i", chunk_no) 796 try: 797 del self.send_chunks_in_progress[chunk_id] 798 except KeyError: 799 pass 840 self.cancel_sending(chunk_id) 800 841 842 def cancel_sending(self, chunk_id): 843 chunk_state = self.send_chunks_in_progress.pop(chunk_id, None) 844 if chunk_state: 845 timer = chunk_state[3] 846 if timer: 847 self.source_remove(timer) 848 chunk_state[3] = 0 849 801 850 def _process_ack_file_chunk(self, packet): 802 851 #the other end received our send-file or send-file-chunk, 803 852 #send some more file data … … 804 853 filelog("ack-file-chunk: %s", packet[1:]) 805 854 chunk_id, state, error_message, chunk = packet[1:5] 806 855 if not state: 807 filelog. error("Error:remote end is cancelling the file transfer:")808 filelog. error(" %s", error_message)809 del self.send_chunks_in_progress[chunk_id]856 filelog.info("the remote end is cancelling the file transfer:") 857 filelog.info(" %s", bytestostr(error_message)) 858 self.cancel_sending(chunk_id) 810 859 return 811 860 chunk_id = bytestostr(chunk_id) 812 861 chunk_state = self.send_chunks_in_progress.get(chunk_id) … … 815 864 return 816 865 if chunk_state[-1]!=chunk: 817 866 filelog.error("Error: chunk number mismatch (%i vs %i)", chunk_state, chunk) 818 del self.send_chunks_in_progress[chunk_id]867 self.cancel_sending(chunk_id) 819 868 return 820 869 start_time, data, chunk_size, timer, chunk = chunk_state 821 870 if not data: … … 823 872 elapsed = monotonic_time()-start_time 824 873 filelog("%i chunks of %i bytes sent in %ims (%sB/s)", 825 874 chunk, chunk_size, elapsed*1000, std_unit(chunk*chunk_size/elapsed)) 826 del self.send_chunks_in_progress[chunk_id]875 self.cancel_sending(chunk_id) 827 876 return 828 877 assert chunk_size>0 829 878 #carve out another chunk: … … 841 890 842 891 def compressed_wrapper(self, datatype, data, level=5): 843 892 raise NotImplementedError() 893 894 def transfer_progress_update(self, send=True, transfer_id=0, elapsed=0, position=0, total=0): 895 #this method is overriden in the gtk client: 896 filelog("transfer_progress_update%s", (send, transfer_id, elapsed, position, total))