diff options
author | Miguel Jacq <mig@mig5.net> | 2018-10-01 16:42:54 +1000 |
---|---|---|
committer | Miguel Jacq <mig@mig5.net> | 2018-10-01 16:42:54 +1000 |
commit | f653e8cc04f09427afb219a82dbf7cf33090d45a (patch) | |
tree | 62c5f1c0c88b0083e6c8d3bed3e29e026c660c19 /onionshare_gui | |
parent | 997e2f87ee003616388b96e65471500490a773ba (diff) | |
parent | 2ffcdbb1083dece7664792f7bef9dbf2245e549e (diff) | |
download | onionshare-f653e8cc04f09427afb219a82dbf7cf33090d45a.tar.gz onionshare-f653e8cc04f09427afb219a82dbf7cf33090d45a.zip |
Merge develop in and fix upload/timer functionality so that it works as described. Still needs fixing to not throw a connection error to the lucky last uploader after their upload completes and server stops due to expiry having passed
Diffstat (limited to 'onionshare_gui')
-rw-r--r-- | onionshare_gui/__init__.py | 10 | ||||
-rw-r--r-- | onionshare_gui/mode.py | 72 | ||||
-rw-r--r-- | onionshare_gui/onion_thread.py | 45 | ||||
-rw-r--r-- | onionshare_gui/onionshare_gui.py | 9 | ||||
-rw-r--r-- | onionshare_gui/receive_mode/__init__.py | 42 | ||||
-rw-r--r-- | onionshare_gui/receive_mode/uploads.py | 28 | ||||
-rw-r--r-- | onionshare_gui/server_status.py | 26 | ||||
-rw-r--r-- | onionshare_gui/settings_dialog.py | 2 | ||||
-rw-r--r-- | onionshare_gui/share_mode/__init__.py | 79 | ||||
-rw-r--r-- | onionshare_gui/share_mode/downloads.py | 18 | ||||
-rw-r--r-- | onionshare_gui/share_mode/threads.py | 60 | ||||
-rw-r--r-- | onionshare_gui/threads.py | 77 |
12 files changed, 284 insertions, 184 deletions
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 13f0e8c7..99db635a 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ from __future__ import division -import os, sys, platform, argparse +import os +import sys +import platform +import argparse +import signal from .widgets import Alert from PyQt5 import QtCore, QtWidgets @@ -58,6 +62,10 @@ def main(): strings.load_strings(common) print(strings._('version_string').format(common.version)) + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Start the Qt app global qtapp qtapp = Application(common) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 418afffd..6b156f7e 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -17,15 +17,13 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ -import time -import threading from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer from .server_status import ServerStatus -from .onion_thread import OnionThread +from .threads import OnionThread from .widgets import Alert class Mode(QtWidgets.QWidget): @@ -39,7 +37,7 @@ class Mode(QtWidgets.QWidget): starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None): + def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() self.common = common self.qtapp = qtapp @@ -56,8 +54,15 @@ class Mode(QtWidgets.QWidget): # The web object gets created in init() self.web = None + # Local mode is passed from OnionShareGui + self.local_only = local_only + + # Threads start out as None + self.onion_thread = None + self.web_thread = None + # Server status - self.server_status = ServerStatus(self.common, self.qtapp, self.app) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_canceled.connect(self.cancel_server) @@ -76,7 +81,15 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) - self.setLayout(self.layout) + # Hack to allow a minimum width on self.layout + min_width_widget = QtWidgets.QWidget() + min_width_widget.setMinimumWidth(450) + self.layout.addWidget(min_width_widget) + + self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() + self.horizontal_layout_wrapper.addLayout(self.layout) + + self.setLayout(self.horizontal_layout_wrapper) def init(self): """ @@ -138,34 +151,11 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') - # Start the onion service in a new thread - def start_onion_service(self): - # Choose a port for the web app - self.app.choose_port() - - # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) - t.daemon = True - t.start() - - # Wait for the web app slug to generate before continuing - if not self.common.settings.get('public_mode'): - while self.web.slug == None: - time.sleep(0.1) - - # Now start the onion service - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except Exception as e: - self.starting_server_error.emit(e.args[0]) - return - self.common.log('Mode', 'start_server', 'Starting an onion thread') - self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) - self.t.daemon = True - self.t.start() + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() def start_server_custom(self): """ @@ -243,10 +233,22 @@ class Mode(QtWidgets.QWidget): """ Cancel the server while it is preparing to start """ - if self.t: - self.t.quit() + self.cancel_server_custom() + + if self.onion_thread: + self.common.log('Mode', 'cancel_server: quitting onion thread') + self.onion_thread.quit() + if self.web_thread: + self.common.log('Mode', 'cancel_server: quitting web thread') + self.web_thread.quit() self.stop_server() + def cancel_server_custom(self): + """ + Add custom initialization here. + """ + pass + def stop_server(self): """ Stop the onionshare server. diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py deleted file mode 100644 index 0a25e891..00000000 --- a/onionshare_gui/onion_thread.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see <http://www.gnu.org/licenses/>. -""" -from PyQt5 import QtCore - -class OnionThread(QtCore.QThread): - """ - A QThread for starting our Onion Service. - By using QThread rather than threading.Thread, we are able - to call quit() or terminate() on the startup if the user - decided to cancel (in which case do not proceed with obtaining - the Onion address and starting the web server). - """ - def __init__(self, common, function, kwargs=None): - super(OnionThread, self).__init__() - - self.common = common - - self.common.log('OnionThread', '__init__') - self.function = function - if not kwargs: - self.kwargs = {} - else: - self.kwargs = kwargs - - def run(self): - self.common.log('OnionThread', 'run') - - self.function(**self.kwargs) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index b63119bb..83f3a7e0 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -55,7 +55,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - self.setMinimumWidth(450) + self.setMinimumWidth(850) # Load settings self.config = config @@ -121,7 +121,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setStatusBar(self.status_bar) # Share mode - self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) + self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only) self.share_mode.init() self.share_mode.server_status.server_started.connect(self.update_server_status_indicator) self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator) @@ -135,7 +135,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.set_server_active.connect(self.set_server_active) # Receive mode - self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray) + self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only) self.receive_mode.init() self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator) self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator) @@ -205,9 +205,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_server_status_indicator() - # Wait 1ms for the event loop to finish, then adjust size - QtCore.QTimer.singleShot(1, self.adjustSize) - def share_mode_clicked(self): if self.mode != self.MODE_SHARE: self.common.log('OnionShareGui', 'share_mode_clicked') diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index fd644d5b..faa65ffe 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -34,7 +34,7 @@ class ReceiveMode(Mode): Custom initialization for ReceiveMode. """ # Create the Web object - self.web = Web(self.common, True, True) + self.web = Web(self.common, True, 'receive') # Server status self.server_status.set_mode('receive') @@ -46,19 +46,13 @@ class ReceiveMode(Mode): self.server_status.web = self.web self.server_status.update() - # Downloads + # Uploads self.uploads = Uploads(self.common) self.uploads_in_progress = 0 self.uploads_completed = 0 self.new_upload = False # For scrolling to the bottom of the uploads list # Information about share, and show uploads button - self.info_show_uploads = QtWidgets.QToolButton() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) - self.info_show_uploads.setCheckable(True) - self.info_show_uploads.toggled.connect(self.uploads_toggled) - self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True)) - self.info_in_progress_uploads_count = QtWidgets.QLabel() self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -72,7 +66,6 @@ class ReceiveMode(Mode): self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_uploads_count) self.info_layout.addWidget(self.info_completed_uploads_count) - self.info_layout.addWidget(self.info_show_uploads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -86,6 +79,8 @@ class ReceiveMode(Mode): # Layout self.layout.insertWidget(0, self.receive_info) self.layout.insertWidget(0, self.info_widget) + self.layout.addStretch() + self.horizontal_layout_wrapper.addWidget(self.uploads) def get_stop_server_shutdown_timeout_text(self): """ @@ -97,16 +92,15 @@ class ReceiveMode(Mode): """ The shutdown timer expired, should we stop the server? Returns a bool """ - # TODO: wait until the final upload is done before stoppign the server? # If there were no attempts to upload files, or all uploads are done, we can stop - if self.web.upload_count == 0 or not self.web.uploads_in_progress: + if self.web.receive_mode.upload_count == 0 or not self.web.receive_mode.uploads_in_progress: self.server_status.stop_server() self.server_status_label.setText(strings._('close_on_timeout', True)) return True # An upload is probably still running - hold off on stopping the share, but block new shares. else: self.server_status_label.setText(strings._('timeout_upload_still_running', True)) - self.web.can_upload = False + self.web.receive_mode.can_upload = False return False return True @@ -116,7 +110,7 @@ class ReceiveMode(Mode): Starting the server. """ # Reset web counters - self.web.upload_count = 0 + self.web.receive_mode.upload_count = 0 self.web.error404_count = 0 # Hide and reset the uploads if we have previously shared @@ -177,6 +171,12 @@ class ReceiveMode(Mode): Handle REQUEST_UPLOAD_FINISHED event. """ self.uploads.finished(event["data"]["id"]) + # Update the total 'completed uploads' info + self.uploads_completed += 1 + self.update_uploads_completed() + # Update the 'in progress uploads' info + self.uploads_in_progress -= 1 + self.update_uploads_in_progress() def on_reload_settings(self): """ @@ -193,12 +193,11 @@ class ReceiveMode(Mode): self.uploads_in_progress = 0 self.update_uploads_completed() self.update_uploads_in_progress() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) self.uploads.reset() def update_uploads_completed(self): """ - Update the 'Downloads completed' info widget. + Update the 'Uploads completed' info widget. """ if self.uploads_completed == 0: image = self.common.get_resource_path('images/share_completed_none.png') @@ -209,13 +208,12 @@ class ReceiveMode(Mode): def update_uploads_in_progress(self): """ - Update the 'Downloads in progress' info widget. + Update the 'Uploads in progress' info widget. """ if self.uploads_in_progress == 0: image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png'))) self.info_in_progress_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_in_progress)) self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress)) @@ -230,13 +228,3 @@ class ReceiveMode(Mode): # Resize window self.adjustSize() - - def uploads_toggled(self, checked): - """ - When the 'Show/hide uploads' button is toggled, show or hide the uploads window. - """ - self.common.log('ReceiveMode', 'toggle_uploads') - if checked: - self.uploads.show() - else: - self.uploads.hide() diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 09834156..48574cc7 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. """ import os import subprocess +import textwrap from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui @@ -221,16 +222,20 @@ class Uploads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_uploads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() + self.uploads_layout = QtWidgets.QVBoxLayout() @@ -238,11 +243,18 @@ class Uploads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(uploads_label) layout.addWidget(self.no_uploads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.uploads_layout) layout.addStretch() widget.setLayout(layout) self.setWidget(widget) + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + def add(self, upload_id, content_length): """ Add a new upload. @@ -250,15 +262,14 @@ class Uploads(QtWidgets.QScrollArea): self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) # Hide the no_uploads_label self.no_uploads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list upload = Upload(self.common, upload_id, content_length) self.uploads[upload_id] = upload self.uploads_layout.addWidget(upload) - # Scroll to the bottom - self.vbar.setValue(self.vbar.maximum()) - def update(self, upload_id, progress): """ Update the progress of an upload. @@ -290,10 +301,12 @@ class Uploads(QtWidgets.QScrollArea): """ self.common.log('Uploads', 'reset') for upload in self.uploads.values(): + upload.close() self.uploads_layout.removeWidget(upload) self.uploads = {} self.no_uploads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) def resizeEvent(self, event): @@ -301,10 +314,7 @@ class Uploads(QtWidgets.QScrollArea): try: for upload in self.uploads.values(): for item in upload.files.values(): - if item.filename_label_width > width: - item.filename_label.setText(item.filename[:25] + '[...]') - item.adjustSize() - if width > item.filename_label_width: - item.filename_label.setText(item.filename) + item.filename_label.setText(textwrap.fill(item.filename, 30)) + item.adjustSize() except: pass diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 9afceb38..32135ca4 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, file_selection=None): + def __init__(self, common, qtapp, app, file_selection=None, local_only=False): super(ServerStatus, self).__init__() self.common = common @@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.local_only = local_only self.resizeEvent(None) # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout = QtWidgets.QDateTimeEdit() - # Set proposed timeout to be 5 minutes into the future self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if self.local_only: + # For testing + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timeout to be 5 minutes into the future + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) shutdown_timeout_layout = QtWidgets.QHBoxLayout() shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) @@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget): Reset the timeout in the UI after stopping a share """ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if not self.local_only: + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) def update(self): """ @@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget): """ if self.status == self.STATUS_STOPPED: if self.common.settings.get('shutdown_timeout'): - # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + if self.local_only: + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + else: + # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index bf0f12ea..1cec527b 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -734,7 +734,7 @@ class SettingsDialog(QtWidgets.QDialog): onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work - Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) + Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) # Clean up onion.cleanup() diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 37315bbe..90fce49a 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -17,7 +17,6 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ -import threading import os from PyQt5 import QtCore, QtWidgets, QtGui @@ -28,6 +27,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads +from .threads import CompressThread from ..mode import Mode from ..widgets import Alert @@ -39,8 +39,11 @@ class ShareMode(Mode): """ Custom initialization for ReceiveMode. """ + # Threads start out as None + self.compress_thread = None + # Create the Web object - self.web = Web(self.common, True, False) + self.web = Web(self.common, True, 'share') # File selection self.file_selection = FileSelection(self.common) @@ -76,12 +79,6 @@ class ShareMode(Mode): self.info_label = QtWidgets.QLabel() self.info_label.setStyleSheet(self.common.css['mode_info_label']) - self.info_show_downloads = QtWidgets.QToolButton() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) - self.info_show_downloads.setCheckable(True) - self.info_show_downloads.toggled.connect(self.downloads_toggled) - self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) - self.info_in_progress_downloads_count = QtWidgets.QLabel() self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -96,7 +93,6 @@ class ShareMode(Mode): self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_downloads_count) self.info_layout.addWidget(self.info_completed_downloads_count) - self.info_layout.addWidget(self.info_show_downloads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -113,6 +109,7 @@ class ShareMode(Mode): # Layout self.layout.insertLayout(0, self.file_selection) self.layout.insertWidget(0, self.info_widget) + self.horizontal_layout_wrapper.addWidget(self.downloads) # Always start with focus on file selection self.file_selection.setFocus() @@ -128,7 +125,7 @@ class ShareMode(Mode): The shutdown timer expired, should we stop the server? Returns a bool """ # If there were no attempts to download the share, or all downloads are done, we can stop - if self.web.download_count == 0 or self.web.done: + if self.web.share_mode.download_count == 0 or self.web.done: self.server_status.stop_server() self.server_status_label.setText(strings._('close_on_timeout', True)) return True @@ -142,7 +139,7 @@ class ShareMode(Mode): Starting the server. """ # Reset web counters - self.web.download_count = 0 + self.web.share_mode.download_count = 0 self.web.error404_count = 0 # Hide and reset the downloads if we have previously shared @@ -161,28 +158,13 @@ class ShareMode(Mode): self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) - # Prepare the files for sending in a new thread - def finish_starting_server(self): - # Prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - - try: - self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(self.web.zip_filename) - - # Only continue if the server hasn't been canceled - if self.server_status.status != self.server_status.STATUS_STOPPED: - self.starting_server_step3.emit() - self.start_server_finished.emit() - except OSError as e: - self.starting_server_error.emit(e.strerror) - return - - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() + # prepare the files for sending in a new thread + self.compress_thread = CompressThread(self) + self.compress_thread.success.connect(self.starting_server_step3.emit) + self.compress_thread.success.connect(self.start_server_finished.emit) + self.compress_thread.error.connect(self.starting_server_error.emit) + self.server_status.server_canceled.connect(self.compress_thread.cancel) + self.compress_thread.start() def start_server_step3_custom(self): """ @@ -195,7 +177,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.zip_filesize >= 157286400: # 150mb + if self.web.share_mode.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -222,6 +204,14 @@ class ShareMode(Mode): self.update_downloads_in_progress() self.file_selection.file_list.adjustSize() + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') + self.compress_thread.quit() + def handle_tor_broke_custom(self): """ Connection to Tor broke. @@ -239,7 +229,11 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.zip_filesize) + if event["data"]["use_gzip"]: + filesize = self.web.share_mode.gzip_filesize + else: + filesize = self.web.share_mode.download_filesize + self.downloads.add(event["data"]["id"], filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -252,7 +246,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.zip_filesize: + if event["data"]["bytes"] == self.web.share_mode.filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info @@ -319,16 +313,6 @@ class ShareMode(Mode): # Resize window self.adjustSize() - def downloads_toggled(self, checked): - """ - When the 'Show/hide downloads' button is toggled, show or hide the downloads window. - """ - self.common.log('ShareMode', 'toggle_downloads') - if checked: - self.downloads.show() - else: - self.downloads.hide() - def reset_info_counters(self): """ Set the info counters back to zero. @@ -337,7 +321,6 @@ class ShareMode(Mode): self.downloads_in_progress = 0 self.update_downloads_completed() self.update_downloads_in_progress() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.downloads.reset() def update_downloads_completed(self): @@ -359,7 +342,6 @@ class ShareMode(Mode): image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png'))) self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_in_progress)) self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) @@ -410,6 +392,7 @@ class ZipProgressBar(QtWidgets.QProgressBar): def update_processed_size(self, val): self._processed_size = val + if self.processed_size < self.total_files_size: self.setValue(int((self.processed_size * 100) / self.total_files_size)) elif self.total_files_size != 0: diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index f5e8512e..a34796f1 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -91,16 +91,19 @@ class Downloads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_downloads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() self.downloads_layout = QtWidgets.QVBoxLayout() @@ -108,26 +111,32 @@ class Downloads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(downloads_label) layout.addWidget(self.no_downloads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.downloads_layout) layout.addStretch() widget.setLayout(layout) self.setWidget(widget) + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + def add(self, download_id, total_bytes): """ Add a new download progress bar. """ # Hide the no_downloads_label self.no_downloads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list download = Download(self.common, download_id, total_bytes) self.downloads[download_id] = download self.downloads_layout.addWidget(download.progress_bar) - # Scroll to the bottom - self.vbar.setValue(self.vbar.maximum()) - def update(self, download_id, downloaded_bytes): """ Update the progress of a download progress bar. @@ -150,4 +159,5 @@ class Downloads(QtWidgets.QScrollArea): self.downloads = {} self.no_downloads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py new file mode 100644 index 00000000..d6022746 --- /dev/null +++ b/onionshare_gui/share_mode/threads.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +from PyQt5 import QtCore + + +class CompressThread(QtCore.QThread): + """ + Compresses files to be shared + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(CompressThread, self).__init__() + self.mode = mode + self.mode.common.log('CompressThread', '__init__') + + # prepare files to share + def set_processed_size(self, x): + if self.mode._zip_progress_bar != None: + self.mode._zip_progress_bar.update_processed_size_signal.emit(x) + + def run(self): + self.mode.common.log('CompressThread', 'run') + + try: + if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + self.success.emit() + else: + # Cancelled + pass + + self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames + except OSError as e: + self.error.emit(e.strerror) + + def cancel(self): + self.mode.common.log('CompressThread', 'cancel') + + # Let the Web and ZipWriter objects know that we're canceling compression early + self.mode.web.cancel_compression = True + if self.mode.web.zip_writer: + self.mode.web.zip_writer.cancel_compression = True diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py new file mode 100644 index 00000000..3b05bebf --- /dev/null +++ b/onionshare_gui/threads.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +import time +from PyQt5 import QtCore + +from onionshare.onion import * + + +class OnionThread(QtCore.QThread): + """ + Starts the onion service, and waits for it to finish + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(OnionThread, self).__init__() + self.mode = mode + self.mode.common.log('OnionThread', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + self.mode.common.log('OnionThread', 'run') + + self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') + + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + + +class WebThread(QtCore.QThread): + """ + Starts the web service + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(WebThread, self).__init__() + self.mode = mode + self.mode.common.log('WebThread', '__init__') + + def run(self): + self.mode.common.log('WebThread', 'run') + self.mode.app.choose_port() + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug')) |