- 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
This commit is contained in:
Micah Lee 2019-01-20 15:25:36 -08:00
parent 89ccf0306b
commit b75757ee49
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
8 changed files with 102 additions and 39 deletions

View File

@ -60,26 +60,11 @@ class ReceiveModeWeb(object):
""" """
Upload files. Upload files.
""" """
# Make sure the receive mode dir exists # Figure out what the receive mode dir should be
now = datetime.now() now = datetime.now()
date_dir = now.strftime("%Y-%m-%d") date_dir = now.strftime("%Y-%m-%d")
time_dir = now.strftime("%H.%M.%S") time_dir = now.strftime("%H.%M.%S")
receive_mode_dir = os.path.join(self.common.settings.get('downloads_dir'), date_dir, time_dir) 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[]') files = request.files.getlist('file[]')
filenames = [] filenames = []
@ -134,6 +119,23 @@ class ReceiveModeWeb(object):
'dir': receive_mode_dir '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)) self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
print(strings._('receive_mode_received_file').format(local_path)) print(strings._('receive_mode_received_file').format(local_path))
f.save(local_path) f.save(local_path)
@ -193,6 +195,7 @@ class ReceiveModeWSGIMiddleware(object):
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
environ['web'] = self.web environ['web'] = self.web
environ['stop_q'] = self.web.stop_q
return self.app(environ, start_response) return self.app(environ, start_response)
@ -201,7 +204,8 @@ class ReceiveModeTemporaryFile(object):
A custom TemporaryFile that tells ReceiveModeRequest every time data gets A custom TemporaryFile that tells ReceiveModeRequest every time data gets
written to it, in order to track the progress of uploads. 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_filename = filename
self.onionshare_write_func = write_func self.onionshare_write_func = write_func
self.onionshare_close_func = close_func self.onionshare_close_func = close_func
@ -222,6 +226,11 @@ class ReceiveModeTemporaryFile(object):
""" """
Custom write method that calls out to onionshare_write_func 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) bytes_written = self.f.write(b)
self.onionshare_write_func(self.onionshare_filename, bytes_written) 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): def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ['web'] 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? # Is this a valid upload request?
self.upload_request = False self.upload_request = False
@ -296,19 +311,35 @@ class ReceiveModeRequest(Request):
'complete': False '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): def close(self):
""" """
Closing the request. Closing the request.
""" """
super(ReceiveModeRequest, self).close() 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: try:
upload_id = self.upload_id upload_id = self.upload_id
# Inform the GUI that the upload has finished
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { if not self.web.stop_q.empty():
'id': upload_id # 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) self.web.receive_mode.uploads_in_progress.remove(upload_id)
except AttributeError: except AttributeError:
pass pass

View File

@ -34,11 +34,6 @@ class ShareModeWeb(object):
# one download at a time. # one download at a time.
self.download_in_progress = False 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() self.define_routes()
def define_routes(self): def define_routes(self):
@ -146,9 +141,6 @@ class ShareModeWeb(object):
basename = os.path.basename(self.download_filename) basename = os.path.basename(self.download_filename)
def generate(): def generate():
# The user hasn't canceled the download
self.client_cancel = False
# Starting a new download # Starting a new download
if not self.web.stay_open: if not self.web.stay_open:
self.download_in_progress = True self.download_in_progress = True
@ -160,7 +152,7 @@ class ShareModeWeb(object):
canceled = False canceled = False
while not self.web.done: while not self.web.done:
# The user has canceled the download, so stop serving the file # 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, { self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': download_id 'id': download_id
}) })

View File

@ -39,7 +39,8 @@ class Web(object):
REQUEST_UPLOAD_FILE_RENAMED = 7 REQUEST_UPLOAD_FILE_RENAMED = 7
REQUEST_UPLOAD_SET_DIR = 8 REQUEST_UPLOAD_SET_DIR = 8
REQUEST_UPLOAD_FINISHED = 9 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'): def __init__(self, common, is_gui, mode='share'):
self.common = common self.common = common
@ -58,6 +59,11 @@ class Web(object):
# Are we running in GUI mode? # Are we running in GUI mode?
self.is_gui = is_gui 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? # Are we using receive mode?
self.mode = mode self.mode = mode
if self.mode == 'receive': if self.mode == 'receive':
@ -225,6 +231,13 @@ class Web(object):
self.stay_open = stay_open 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) # 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'): if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0' host = '0.0.0.0'
@ -238,11 +251,10 @@ class Web(object):
""" """
Stop the flask web server by loading /shutdown. Stop the flask web server by loading /shutdown.
""" """
self.common.log('Web', 'stop', 'stopping server')
if self.mode == 'share': # Let the mode know that the user stopped the server
# If the user cancels the download, let the download function know to stop self.stop_q.put(True)
# serving the file
self.share_mode.client_cancel = True
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown # To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
if self.running: if self.running:

View File

@ -335,3 +335,9 @@ class Mode(QtWidgets.QWidget):
Handle REQUEST_UPLOAD_FINISHED event. Handle REQUEST_UPLOAD_FINISHED event.
""" """
pass pass
def handle_request_upload_canceled(self, event):
"""
Handle REQUEST_UPLOAD_CANCELED event.
"""
pass

View File

@ -184,7 +184,7 @@ class UploadHistoryItemFile(QtWidgets.QWidget):
# macOS # macOS
elif self.common.platform == 'Darwin': elif self.common.platform == 'Darwin':
subprocess.call(['open', '-R', abs_filename]) subprocess.call(['open', '-R', abs_filename])
# Windows # Windows
elif self.common.platform == 'Windows': elif self.common.platform == 'Windows':
@ -295,6 +295,13 @@ class UploadHistoryItem(HistoryItem):
) )
self.label.setText(text) 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): class HistoryItemList(QtWidgets.QScrollArea):
""" """

View File

@ -83,7 +83,7 @@ class ReceiveMode(Mode):
# Wrapper layout # Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout) 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) self.setLayout(self.wrapper_layout)
def get_stop_server_shutdown_timeout_text(self): def get_stop_server_shutdown_timeout_text(self):
@ -198,6 +198,18 @@ class ReceiveMode(Mode):
self.history.update_completed() self.history.update_completed()
self.history.update_in_progress() 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): def on_reload_settings(self):
""" """
We should be ok to re-enable the 'Start Receive Mode' button now. We should be ok to re-enable the 'Start Receive Mode' button now.

View File

@ -115,7 +115,7 @@ class ShareMode(Mode):
# Wrapper layout # Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout) 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) self.setLayout(self.wrapper_layout)
# Always start with focus on file selection # Always start with focus on file selection

View File

@ -396,6 +396,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
elif event["type"] == Web.REQUEST_UPLOAD_FINISHED: elif event["type"] == Web.REQUEST_UPLOAD_FINISHED:
mode.handle_request_upload_finished(event) 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: 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"])) Alert(self.common, strings._('error_cannot_create_downloads_dir').format(event["data"]["receive_mode_dir"]))