From b75757ee49f58f77f524da9ec32cda8062697628 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Jan 2019 15:25:36 -0800 Subject: - Refactor the Web.ShareMode client_cancel variable to be Web.stop_q, a thread-safe queue that communicates to both share and receive mode when the user stops the server. In share mode this still stops sending the file. In receive mode, if there's a transfer in progress, it cancels it in the middle, and doesn't end up saving that file - In receive mode, make the receive mode dir right before saving a file (so if it doesn't complete, don't make an empty dir) - Minor UX tweak: resizing the window stretches the History widget first --- onionshare/web/receive_mode.py | 75 ++++++++++++++++++++-------- onionshare/web/share_mode.py | 10 +--- onionshare/web/web.py | 22 ++++++-- onionshare_gui/mode/__init__.py | 6 +++ onionshare_gui/mode/history.py | 9 +++- onionshare_gui/mode/receive_mode/__init__.py | 14 +++++- onionshare_gui/mode/share_mode/__init__.py | 2 +- onionshare_gui/onionshare_gui.py | 3 ++ 8 files changed, 102 insertions(+), 39 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 6985f38a..e1034af6 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -60,26 +60,11 @@ class ReceiveModeWeb(object): """ Upload files. """ - # Make sure the receive mode dir exists + # Figure out what the receive mode dir should be now = datetime.now() date_dir = now.strftime("%Y-%m-%d") time_dir = now.strftime("%H.%M.%S") receive_mode_dir = os.path.join(self.common.settings.get('downloads_dir'), date_dir, time_dir) - valid = True - try: - os.makedirs(receive_mode_dir, 0o700) - except PermissionError: - self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path, { - "receive_mode_dir": receive_mode_dir - }) - print(strings._('error_cannot_create_downloads_dir').format(receive_mode_dir)) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) files = request.files.getlist('file[]') filenames = [] @@ -134,6 +119,23 @@ class ReceiveModeWeb(object): 'dir': receive_mode_dir }) + # Make sure receive mode dir exists before writing file + valid = True + try: + os.makedirs(receive_mode_dir, 0o700) + except PermissionError: + self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path, { + "receive_mode_dir": receive_mode_dir + }) + print(strings._('error_cannot_create_downloads_dir').format(receive_mode_dir)) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if self.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) print(strings._('receive_mode_received_file').format(local_path)) f.save(local_path) @@ -193,6 +195,7 @@ class ReceiveModeWSGIMiddleware(object): def __call__(self, environ, start_response): environ['web'] = self.web + environ['stop_q'] = self.web.stop_q return self.app(environ, start_response) @@ -201,7 +204,8 @@ class ReceiveModeTemporaryFile(object): A custom TemporaryFile that tells ReceiveModeRequest every time data gets written to it, in order to track the progress of uploads. """ - def __init__(self, filename, write_func, close_func): + def __init__(self, request, filename, write_func, close_func): + self.onionshare_request = request self.onionshare_filename = filename self.onionshare_write_func = write_func self.onionshare_close_func = close_func @@ -222,6 +226,11 @@ class ReceiveModeTemporaryFile(object): """ Custom write method that calls out to onionshare_write_func """ + if not self.onionshare_request.stop_q.empty(): + self.close() + self.onionshare_request.close() + return + bytes_written = self.f.write(b) self.onionshare_write_func(self.onionshare_filename, bytes_written) @@ -241,6 +250,12 @@ class ReceiveModeRequest(Request): def __init__(self, environ, populate_request=True, shallow=False): super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) self.web = environ['web'] + self.stop_q = environ['stop_q'] + + self.web.common.log('ReceiveModeRequest', '__init__') + + # Prevent running the close() method more than once + self.closed = False # Is this a valid upload request? self.upload_request = False @@ -296,19 +311,35 @@ class ReceiveModeRequest(Request): 'complete': False } - return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func) + return ReceiveModeTemporaryFile(self, filename, self.file_write_func, self.file_close_func) def close(self): """ Closing the request. """ super(ReceiveModeRequest, self).close() + + # Prevent calling this method more than once per request + if self.closed: + return + self.closed = True + + self.web.common.log('ReceiveModeRequest', 'close') + try: upload_id = self.upload_id - # Inform the GUI that the upload has finished - self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { - 'id': upload_id - }) + + if not self.web.stop_q.empty(): + # Inform the GUI that the upload has canceled + self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, { + 'id': upload_id + }) + else: + # Inform the GUI that the upload has finished + self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { + 'id': upload_id + }) + self.web.receive_mode.uploads_in_progress.remove(upload_id) except AttributeError: pass diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index a57d0a39..eb487c42 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -34,11 +34,6 @@ class ShareModeWeb(object): # one download at a time. self.download_in_progress = False - # If the client closes the OnionShare window while a download is in progress, - # it should immediately stop serving the file. The client_cancel global is - # used to tell the download function that the client is canceling the download. - self.client_cancel = False - self.define_routes() def define_routes(self): @@ -146,9 +141,6 @@ class ShareModeWeb(object): basename = os.path.basename(self.download_filename) def generate(): - # The user hasn't canceled the download - self.client_cancel = False - # Starting a new download if not self.web.stay_open: self.download_in_progress = True @@ -160,7 +152,7 @@ class ShareModeWeb(object): canceled = False while not self.web.done: # The user has canceled the download, so stop serving the file - if self.client_cancel: + if not self.web.stop_q.empty(): self.web.add_request(self.web.REQUEST_CANCELED, path, { 'id': download_id }) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 0f156941..2ea5ff35 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -39,7 +39,8 @@ class Web(object): REQUEST_UPLOAD_FILE_RENAMED = 7 REQUEST_UPLOAD_SET_DIR = 8 REQUEST_UPLOAD_FINISHED = 9 - REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 10 + REQUEST_UPLOAD_CANCELED = 10 + REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 11 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -58,6 +59,11 @@ class Web(object): # Are we running in GUI mode? self.is_gui = is_gui + # If the user stops the server while a transfer is in progress, it should + # immediately stop the transfer. In order to make it thread-safe, stop_q + # is a queue. If anything is in it, then the user stopped the server + self.stop_q = queue.Queue() + # Are we using receive mode? self.mode = mode if self.mode == 'receive': @@ -225,6 +231,13 @@ class Web(object): self.stay_open = stay_open + # Make sure the stop_q is empty when starting a new server + while not self.stop_q.empty(): + try: + self.stop_q.get(block=False) + except queue.Empty: + pass + # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) if os.path.exists('/usr/share/anon-ws-base-files/workstation'): host = '0.0.0.0' @@ -238,11 +251,10 @@ class Web(object): """ Stop the flask web server by loading /shutdown. """ + self.common.log('Web', 'stop', 'stopping server') - if self.mode == 'share': - # If the user cancels the download, let the download function know to stop - # serving the file - self.share_mode.client_cancel = True + # Let the mode know that the user stopped the server + self.stop_q.put(True) # To stop flask, load http://127.0.0.1://shutdown if self.running: diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index 5110289f..bd363915 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -335,3 +335,9 @@ class Mode(QtWidgets.QWidget): Handle REQUEST_UPLOAD_FINISHED event. """ pass + + def handle_request_upload_canceled(self, event): + """ + Handle REQUEST_UPLOAD_CANCELED event. + """ + pass diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index e72a3838..5f895d75 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -184,7 +184,7 @@ class UploadHistoryItemFile(QtWidgets.QWidget): # macOS elif self.common.platform == 'Darwin': - subprocess.call(['open', '-R', abs_filename]) + subprocess.call(['open', '-R', abs_filename]) # Windows elif self.common.platform == 'Windows': @@ -295,6 +295,13 @@ class UploadHistoryItem(HistoryItem): ) self.label.setText(text) + elif data['action'] == 'canceled': + # Hide the progress bar + self.progress_bar.hide() + + # Change the label + self.label.setText(strings._('gui_canceled')) + class HistoryItemList(QtWidgets.QScrollArea): """ diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index c53f1ea1..cde2cb8a 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -83,7 +83,7 @@ class ReceiveMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history) + self.wrapper_layout.addWidget(self.history, stretch=1) self.setLayout(self.wrapper_layout) def get_stop_server_shutdown_timeout_text(self): @@ -198,6 +198,18 @@ class ReceiveMode(Mode): self.history.update_completed() self.history.update_in_progress() + def handle_request_upload_canceled(self, event): + """ + Handle REQUEST_UPLOAD_CANCELED event. + """ + self.history.update(event["data"]["id"], { + 'action': 'canceled' + }) + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() + def on_reload_settings(self): """ We should be ok to re-enable the 'Start Receive Mode' button now. diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 0cc00f92..d86a17e4 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -115,7 +115,7 @@ class ShareMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history) + self.wrapper_layout.addWidget(self.history, stretch=1) self.setLayout(self.wrapper_layout) # Always start with focus on file selection diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index eab3261e..c4f69b2d 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -396,6 +396,9 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == Web.REQUEST_UPLOAD_FINISHED: mode.handle_request_upload_finished(event) + elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: + mode.handle_request_upload_canceled(event) + if event["type"] == Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE: Alert(self.common, strings._('error_cannot_create_downloads_dir').format(event["data"]["receive_mode_dir"])) -- cgit v1.2.3-54-g00ecf From 634b8ecebd72c1108ad99bcbe3c096cf6cd14a94 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Jan 2019 15:49:08 -0800 Subject: When canceling a receive mode transfer, display date range in the UI --- onionshare_gui/mode/history.py | 21 +++++++++++++++++---- share/locale/en.json | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 02c34ec4..0ebd8b93 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -45,19 +45,32 @@ class HistoryItem(QtWidgets.QWidget): When an item finishes, returns a string displaying the start/end datetime range. started is a datetime object. """ + return self._get_label_text('gui_all_modes_transfer_finished', 'gui_all_modes_transfer_finished_range', started) + + def get_canceled_label_text(self, started): + """ + When an item is canceled, returns a string displaying the start/end datetime range. + started is a datetime object. + """ + return self._get_label_text('gui_all_modes_transfer_canceled', 'gui_all_modes_transfer_canceled_range', started) + + def _get_label_text(self, string_name, string_range_name, started): + """ + Return a string that contains a date, or date range. + """ ended = datetime.now() if started.year == ended.year and started.month == ended.month and started.day == ended.day: if started.hour == ended.hour and started.minute == ended.minute: - text = strings._('gui_all_modes_transfer_finished').format( + text = strings._(string_name).format( started.strftime("%b %d, %I:%M%p") ) else: - text = strings._('gui_all_modes_transfer_finished_range').format( + text = strings._(string_range_name).format( started.strftime("%b %d, %I:%M%p"), ended.strftime("%I:%M%p") ) else: - text = strings._('gui_all_modes_transfer_finished_range').format( + text = strings._(string_range_name).format( started.strftime("%b %d, %I:%M%p"), ended.strftime("%b %d, %I:%M%p") ) @@ -311,7 +324,7 @@ class ReceiveHistoryItem(HistoryItem): self.progress_bar.hide() # Change the label - self.label.setText(strings._('gui_canceled')) + self.label.setText(self.get_canceled_label_text(self.started)) class HistoryItemList(QtWidgets.QScrollArea): diff --git a/share/locale/en.json b/share/locale/en.json index a3307e49..3ad2efda 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -171,6 +171,8 @@ "gui_all_modes_transfer_started": "Started {}", "gui_all_modes_transfer_finished_range": "Transferred {} - {}", "gui_all_modes_transfer_finished": "Transferred {}", + "gui_all_modes_transfer_canceled_range": "Canceled {} - {}", + "gui_all_modes_transfer_canceled": "Canceled {}", "gui_all_modes_progress_complete": "%p%, {0:s} elapsed.", "gui_all_modes_progress_starting": "{0:s}, %p% (calculating)", "gui_all_modes_progress_eta": "{0:s}, ETA: {1:s}, %p%", -- cgit v1.2.3-54-g00ecf From b6928a6d0edec347f5c39a90078b3deee5102095 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Jan 2019 16:00:18 -0800 Subject: Oops, I missed this when resolving merge conflicts --- onionshare/web/receive_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 82f0de42..a64c2a73 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -124,10 +124,10 @@ class ReceiveModeWeb(object): try: os.makedirs(receive_mode_dir, 0o700) except PermissionError: - self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path, { + self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, { "receive_mode_dir": receive_mode_dir }) - print(strings._('error_cannot_create_downloads_dir').format(receive_mode_dir)) + print(strings._('error_cannot_create_data_dir').format(receive_mode_dir)) valid = False if not valid: flash('Error uploading, please inform the OnionShare user', 'error') -- cgit v1.2.3-54-g00ecf From 1be10f1e0268567c3fc7d19f4b9a1692e8f38fb7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 21 Jan 2019 10:56:31 -0800 Subject: Prevent ReceiveModeRequest.file_write_func from sending a message to the GUI if the request should be closed --- onionshare/web/receive_mode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index a64c2a73..ad72c9f4 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -348,6 +348,9 @@ class ReceiveModeRequest(Request): """ This function gets called when a specific file is written to. """ + if self.closed: + return + if self.upload_request: self.progress[filename]['uploaded_bytes'] += length -- cgit v1.2.3-54-g00ecf From 2fcedb8730c70e15e940dee9d2e346b46574a20f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 21 Jan 2019 17:32:58 -0800 Subject: Oops, finish resolving merge conflict --- onionshare/web/receive_mode.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 450417f6..6124ecf1 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -347,29 +347,21 @@ class ReceiveModeRequest(Request): self.web.common.log('ReceiveModeRequest', 'close') try: -<<<<<<< HEAD - upload_id = self.upload_id - - if not self.web.stop_q.empty(): - # Inform the GUI that the upload has canceled - self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, { - 'id': upload_id - }) - else: -======= if self.told_gui_about_request: upload_id = self.upload_id ->>>>>>> develop - # Inform the GUI that the upload has finished - self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { - 'id': upload_id - }) -<<<<<<< HEAD - self.web.receive_mode.uploads_in_progress.remove(upload_id) -======= + if not self.web.stop_q.empty(): + # Inform the GUI that the upload has canceled + self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, { + 'id': upload_id + }) + else: + # Inform the GUI that the upload has finished + self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { + 'id': upload_id + }) self.web.receive_mode.uploads_in_progress.remove(upload_id) ->>>>>>> develop + except AttributeError: pass -- cgit v1.2.3-54-g00ecf From f854b3ff372597664d4ef5dce0f6ba0d3574b35f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 21 Jan 2019 17:43:13 -0800 Subject: Tests are failing because a receive mode dir already exists, so this makes them pass --- onionshare/web/receive_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 6124ecf1..f035271a 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -137,7 +137,7 @@ class ReceiveModeWeb(object): # Make sure receive mode dir exists before writing file valid = True try: - os.makedirs(receive_mode_dir, 0o700) + os.makedirs(receive_mode_dir, 0o700, exist_ok=True) except PermissionError: self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, { "receive_mode_dir": receive_mode_dir -- cgit v1.2.3-54-g00ecf