diff --git a/onionshare/web/send_base_mode.py b/onionshare/web/send_base_mode.py index a34aedfd..402bc32f 100644 --- a/onionshare/web/send_base_mode.py +++ b/onionshare/web/send_base_mode.py @@ -132,18 +132,28 @@ class SendBaseModeWeb: file_to_download = filesystem_path filesize = os.path.getsize(filesystem_path) - # TODO: Tell GUI the download started - #self.web.add_request(self.web.REQUEST_STARTED, path, { - # 'id': download_id, - # 'use_gzip': use_gzip - #}) + # Each download has a unique id + download_id = self.download_count + self.download_count += 1 + + path = request.path + + # Tell GUI the individual file started + self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, path, { + 'id': download_id, + 'filesize': filesize, + 'method': request.method + }) + + # Only GET requests are allowed, any other method should fail + if request.method != "GET": + return self.web.error405() def generate(): chunk_size = 102400 # 100kb fp = open(file_to_download, 'rb') done = False - canceled = False while not done: chunk = fp.read(chunk_size) if chunk == b'': @@ -152,7 +162,7 @@ class SendBaseModeWeb: try: yield chunk - # TODO: Tell GUI the progress + # Tell GUI the progress downloaded_bytes = fp.tell() percent = (1.0 * downloaded_bytes / filesize) * 100 if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -160,20 +170,19 @@ class SendBaseModeWeb: "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) sys.stdout.flush() - #self.web.add_request(self.web.REQUEST_PROGRESS, path, { - # 'id': download_id, - # 'bytes': downloaded_bytes - # }) + self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) done = False except: # Looks like the download was canceled done = True - canceled = True - # TODO: Tell the GUI the download has canceled - #self.web.add_request(self.web.REQUEST_CANCELED, path, { - # 'id': download_id - #}) + # Tell the GUI the individual file was canceled + self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_CANCELED, path, { + 'id': download_id + }) fp.close() diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 8d5a6af5..5a96b324 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -37,15 +37,18 @@ class Web: REQUEST_LOAD = 0 REQUEST_STARTED = 1 REQUEST_PROGRESS = 2 - REQUEST_OTHER = 3 - REQUEST_CANCELED = 4 - REQUEST_RATE_LIMIT = 5 - REQUEST_UPLOAD_FILE_RENAMED = 6 - REQUEST_UPLOAD_SET_DIR = 7 - REQUEST_UPLOAD_FINISHED = 8 - REQUEST_UPLOAD_CANCELED = 9 - REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 - REQUEST_INVALID_PASSWORD = 11 + REQUEST_CANCELED = 3 + REQUEST_RATE_LIMIT = 4 + REQUEST_UPLOAD_FILE_RENAMED = 5 + REQUEST_UPLOAD_SET_DIR = 6 + REQUEST_UPLOAD_FINISHED = 7 + REQUEST_UPLOAD_CANCELED = 8 + REQUEST_INDIVIDUAL_FILE_STARTED = 9 + REQUEST_INDIVIDUAL_FILE_PROGRESS = 10 + REQUEST_INDIVIDUAL_FILE_CANCELED = 11 + REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12 + REQUEST_OTHER = 13 + REQUEST_INVALID_PASSWORD = 14 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -193,15 +196,18 @@ class Web: r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401) return self.add_security_headers(r) + def error403(self): + self.add_request(Web.REQUEST_OTHER, request.path) + r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403) + return self.add_security_headers(r) + def error404(self): self.add_request(Web.REQUEST_OTHER, request.path) r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404) return self.add_security_headers(r) - def error403(self): - self.add_request(Web.REQUEST_OTHER, request.path) - - r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403) + def error405(self): + r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405) return self.add_security_headers(r) def add_security_headers(self, r): diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index e92e36f8..b5a95f41 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -22,6 +22,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import AutoStopTimer +from .history import IndividualFileHistoryItem + from ..server_status import ServerStatus from ..threads import OnionThread from ..threads import AutoStartTimer @@ -29,7 +31,7 @@ from ..widgets import Alert class Mode(QtWidgets.QWidget): """ - The class that ShareMode and ReceiveMode inherit from. + The class that all modes inherit from """ start_server_finished = QtCore.pyqtSignal() stop_server_finished = QtCore.pyqtSignal() @@ -417,3 +419,46 @@ class Mode(QtWidgets.QWidget): Handle REQUEST_UPLOAD_CANCELED event. """ pass + + def handle_request_individual_file_started(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_STARTED event. + Used in both Share and Website modes, so implemented here. + """ + item = IndividualFileHistoryItem(self.common, event["data"], event["path"]) + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + self.history.in_progress_count += 1 + self.history.update_in_progress() + + def handle_request_individual_file_progress(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event. + Used in both Share and Website modes, so implemented here. + """ + self.history.update(event["data"]["id"], event["data"]["bytes"]) + + # Is the download complete? + if event["data"]["bytes"] == self.web.share_mode.filesize: + # Update completed and in progress labels + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() + + else: + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.history.cancel(event["data"]["id"]) + self.history.in_progress_count = 0 + self.history.update_in_progress() + + def handle_request_individual_file_canceled(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event. + Used in both Share and Website modes, so implemented here. + """ + self.history.cancel(event["data"]["id"]) + + # Update in progress count + self.history.in_progress_count -= 1 + self.history.update_in_progress() diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index c2c696fc..a9fbbb36 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -345,61 +345,88 @@ class IndividualFileHistoryItem(HistoryItem): """ Individual file history item, for share mode viewing of individual files """ - def __init__(self, common, path): + def __init__(self, common, data, path): super(IndividualFileHistoryItem, self).__init__() self.status = HistoryItem.STATUS_STARTED self.common = common - self.visited = time.time() - self.visited_dt = datetime.fromtimestamp(self.visited) + self.id = id + self.path = path + self.method = data['method'] + self.total_bytes = data['filesize'] + self.downloaded_bytes = 0 + self.started = time.time() + self.started_dt = datetime.fromtimestamp(self.started) + self.status = HistoryItem.STATUS_STARTED # Labels - self.timestamp_label = QtWidgets.QLabel(self.visited_dt.strftime("%b %d, %I:%M%p")) - self.path_viewed_label = QtWidgets.QLabel(strings._('gui_individual_file_download').format(path)) + self.timestamp_label = QtWidgets.QLabel(self.started_dt.strftime("%b %d, %I:%M%p")) + self.method_label = QtWidgets.QLabel("{} {}".format(self.method, self.path)) + self.status_label = QtWidgets.QLabel() + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(data['filesize']) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) + self.progress_bar.total_bytes = data['filesize'] + + # Text layout + labels_layout = QtWidgets.QHBoxLayout() + labels_layout.addWidget(self.timestamp_label) + labels_layout.addWidget(self.method_label) + labels_layout.addWidget(self.status_label) # Layout layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.timestamp_label) - layout.addWidget(self.path_viewed_label) + layout.addLayout(labels_layout) + layout.addWidget(self.progress_bar) self.setLayout(layout) + # All non-GET requests are error 405 Method Not Allowed + if self.method.lower() != 'get': + self.status_label.setText("405") + self.progress_bar.hide() + else: + # Start at 0 + self.update(0) - def update(self): - self.label.setText(self.get_finished_label_text(self.started_dt)) - self.status = HistoryItem.STATUS_FINISHED + def update(self, downloaded_bytes): + self.downloaded_bytes = downloaded_bytes + + self.progress_bar.setValue(downloaded_bytes) + if downloaded_bytes == self.progress_bar.total_bytes: + self.progress_bar.hide() + self.status = HistoryItem.STATUS_FINISHED + + else: + elapsed = time.time() - self.started + if elapsed < 10: + # Wait a couple of seconds for the download rate to stabilize. + # This prevents a "Windows copy dialog"-esque experience at + # the beginning of the download. + pb_fmt = strings._('gui_all_modes_progress_starting').format( + self.common.human_readable_filesize(downloaded_bytes)) + else: + pb_fmt = strings._('gui_all_modes_progress_eta').format( + self.common.human_readable_filesize(downloaded_bytes), + self.estimated_time_remaining) + + self.progress_bar.setFormat(pb_fmt) def cancel(self): self.progress_bar.setFormat(strings._('gui_canceled')) self.status = HistoryItem.STATUS_CANCELED -class VisitHistoryItem(HistoryItem): - """ - Download history item, for share mode - """ - def __init__(self, common, id, total_bytes): - super(VisitHistoryItem, self).__init__() - self.status = HistoryItem.STATUS_STARTED - self.common = common - - self.id = id - self.visited = time.time() - self.visited_dt = datetime.fromtimestamp(self.visited) - - # Label - self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p"))) - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.label) - self.setLayout(layout) - - def update(self): - self.label.setText(self.get_finished_label_text(self.started_dt)) - self.status = HistoryItem.STATUS_FINISHED - - def cancel(self): - self.progress_bar.setFormat(strings._('gui_canceled')) - self.status = HistoryItem.STATUS_CANCELED + @property + def estimated_time_remaining(self): + return self.common.estimated_time_remaining(self.downloaded_bytes, + self.total_bytes, + self.started) class HistoryItemList(QtWidgets.QScrollArea): """ diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 56aa1364..b5da0cd3 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -225,21 +225,6 @@ class ShareMode(Mode): """ self.primary_action.hide() - def handle_request_load(self, event): - """ - Handle REQUEST_LOAD event. - """ - self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_page_loaded_message')) - if not self.common.settings.get('close_after_first_download') and not event["path"].startswith(('/favicon.ico', '/download', self.web.static_url_path)) and event["path"] != '/': - - item = IndividualFileHistoryItem(self.common, event["path"]) - - self.history.add(0, item) - self.toggle_history.update_indicator(True) - self.history.completed_count += 1 - self.history.update_completed() - self.system_tray.showMessage(strings._('systray_individual_file_downloaded_title'), strings._('systray_individual_file_downloaded_message').format(event["path"])) - def handle_request_started(self, event): """ Handle REQUEST_STARTED event. diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 8ac88c8c..3d4497f0 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -30,7 +30,7 @@ from onionshare.web import Web from ..file_selection import FileSelection from .. import Mode -from ..history import History, ToggleHistory, VisitHistoryItem +from ..history import History, ToggleHistory from ...widgets import Alert class WebsiteMode(Mode): @@ -204,21 +204,6 @@ class WebsiteMode(Mode): """ self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message')) - def handle_request_started(self, event): - """ - Handle REQUEST_STARTED event. - """ - if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ): - item = VisitHistoryItem(self.common, event["data"]["id"], 0) - - self.history.add(event["data"]["id"], item) - self.toggle_history.update_indicator(True) - self.history.completed_count += 1 - self.history.update_completed() - - self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message')) - - def on_reload_settings(self): """ If there were some files listed for sharing, we should be ok to re-enable diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index bed86895..20873bc8 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -383,7 +383,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.server_status.autostart_timer_container.hide() self.receive_mode.server_status.autostart_timer_container.hide() self.website_mode.server_status.autostart_timer_container.hide() - + d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only) d.settings_saved.connect(reload_settings) d.exec_() @@ -470,6 +470,15 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: mode.handle_request_upload_canceled(event) + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED: + mode.handle_request_individual_file_started(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS: + mode.handle_request_individual_file_progress(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED: + mode.handle_request_individual_file_canceled(event) + if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE: Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"])) diff --git a/share/locale/en.json b/share/locale/en.json index c26577b2..5fbf88f9 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -178,7 +178,6 @@ "gui_receive_mode_no_files": "No Files Received Yet", "gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving", "gui_visit_started": "Someone has visited your website {}", - "gui_individual_file_download": "Viewed {}", "receive_mode_upload_starting": "Upload of total size {} is starting", "days_first_letter": "d", "hours_first_letter": "h", diff --git a/share/templates/405.html b/share/templates/405.html new file mode 100644 index 00000000..55493ae7 --- /dev/null +++ b/share/templates/405.html @@ -0,0 +1,19 @@ + + + + + OnionShare: 405 Method Not Allowed + + + + + +
+
+

+

405 Method Not Allowed

+
+
+ + +