mirror of
https://github.com/onionshare/onionshare.git
synced 2025-04-21 07:56:32 -04:00
Merge f7adcda215cd67042666bd47470ef1057bd71dbd into 16644b009f99e53b8b271c4bd1a50e6260e1935b
This commit is contained in:
commit
fcb25865f1
@ -43,7 +43,7 @@ class ModeSettings:
|
||||
"persistent": {
|
||||
"mode": None,
|
||||
"enabled": False,
|
||||
"autostart_on_launch": False
|
||||
"autostart_on_launch": False,
|
||||
},
|
||||
"general": {
|
||||
"title": None,
|
||||
@ -67,8 +67,9 @@ class ModeSettings:
|
||||
"disable_csp": False,
|
||||
"custom_csp": None,
|
||||
"log_filenames": False,
|
||||
"filenames": []
|
||||
"filenames": [],
|
||||
},
|
||||
"download": {"data_dir": self.build_default_receive_data_dir(), "poll": 0},
|
||||
"chat": {},
|
||||
}
|
||||
self._settings = {}
|
||||
|
@ -190,6 +190,23 @@ class Onion(object):
|
||||
s = key_b32.decode("utf-8")
|
||||
return s
|
||||
|
||||
def private_key_to_b64(self, encoded_key):
|
||||
"""
|
||||
Converts a base32 encoded string back to a base64 encoded string.
|
||||
|
||||
This is used for adding a Private Key to an onion when operating in
|
||||
client mode (such as when downloading from another OnionShare)
|
||||
"""
|
||||
# Decode the base32 string into bytes
|
||||
encoded_key_b32 = encoded_key.encode("utf-8")
|
||||
decoded_bytes = base64.b32decode(encoded_key_b32 + b"====")
|
||||
|
||||
# Convert the decoded bytes to base64
|
||||
base64_key = base64.b64encode(decoded_bytes)
|
||||
|
||||
# Convert to string and return
|
||||
return base64_key.decode("utf-8")
|
||||
|
||||
def connect(
|
||||
self,
|
||||
custom_settings=None,
|
||||
@ -417,7 +434,11 @@ class Onion(object):
|
||||
|
||||
return_code = self.tor_proc.poll()
|
||||
if return_code != None:
|
||||
self.common.log("Onion", "connect", f"tor process has terminated early: {return_code}")
|
||||
self.common.log(
|
||||
"Onion",
|
||||
"connect",
|
||||
f"tor process has terminated early: {return_code}",
|
||||
)
|
||||
|
||||
# Connect to the controller
|
||||
self.common.log("Onion", "connect", "authenticating to tor controller")
|
||||
@ -679,6 +700,31 @@ class Onion(object):
|
||||
else:
|
||||
return False
|
||||
|
||||
def add_onion_client_auth(self, service_id, private_key, key_type="x25519"):
|
||||
"""
|
||||
Implemented from scratch here because it never made it into Stem maint branch
|
||||
"""
|
||||
if self.supports_v3_onions and self.supports_ephemeral:
|
||||
private_key64 = self.private_key_to_b64(private_key)
|
||||
request = f"ONION_CLIENT_AUTH_ADD {service_id} {key_type}:{private_key64}"
|
||||
response = self.c.msg(request)
|
||||
self.common.log("Onion", "add_onion_client_auth", response)
|
||||
return response
|
||||
else:
|
||||
raise TorTooOldEphemeral()
|
||||
|
||||
def remove_onion_client_auth(self, service_id):
|
||||
"""
|
||||
Implemented from scratch here because it never made it into Stem maint branch
|
||||
"""
|
||||
if self.supports_v3_onions and self.supports_ephemeral:
|
||||
request = f"ONION_CLIENT_AUTH_REMOVE {service_id}"
|
||||
response = self.c.msg(request)
|
||||
self.common.log("Onion", "remove_onion_client_auth", response)
|
||||
return response
|
||||
else:
|
||||
raise TorTooOldEphemeral()
|
||||
|
||||
def start_onion_service(self, mode, mode_settings, port, await_publication):
|
||||
"""
|
||||
Start a onion service on port 80, pointing to the given port, and
|
||||
|
@ -43,6 +43,7 @@ from onionshare_cli.onion import (
|
||||
from onionshare_cli.meek import Meek
|
||||
from onionshare_cli.web.web import WaitressException
|
||||
|
||||
|
||||
class GuiCommon:
|
||||
"""
|
||||
The shared code for all of the OnionShare GUI.
|
||||
@ -52,6 +53,7 @@ class GuiCommon:
|
||||
MODE_RECEIVE = "receive"
|
||||
MODE_WEBSITE = "website"
|
||||
MODE_CHAT = "chat"
|
||||
MODE_DOWNLOAD = "download"
|
||||
|
||||
def __init__(self, common, qtapp, local_only):
|
||||
self.common = common
|
||||
@ -388,7 +390,7 @@ class GuiCommon:
|
||||
color: """
|
||||
+ title_color
|
||||
+ """;
|
||||
font-size: 25px;
|
||||
font-size: 16px;
|
||||
}
|
||||
""",
|
||||
# Share mode and child widget styles
|
||||
@ -485,6 +487,41 @@ class GuiCommon:
|
||||
""",
|
||||
}
|
||||
|
||||
def wrap_text(self, item, text):
|
||||
"""
|
||||
Helper function to insert 'fake' line breaks in long strings
|
||||
(such as file names or onion addresses) so that we may then
|
||||
use the resulting string in a label with setWordWrap(True)
|
||||
to properly wrap it.
|
||||
"""
|
||||
# Get the font metrics of the label to calculate line breaks
|
||||
fm = QtGui.QFontMetrics(item.font())
|
||||
|
||||
# Max width of the label
|
||||
max_width = item.maximumWidth()
|
||||
|
||||
# Wrap the text by inserting line breaks
|
||||
lines = []
|
||||
current_line = ""
|
||||
|
||||
for char in text:
|
||||
# Add the character to the current line
|
||||
current_line += char
|
||||
|
||||
# If the line exceeds the maximum width, start a new line
|
||||
if fm.horizontalAdvance(current_line) > max_width:
|
||||
lines.append(
|
||||
current_line[:-1]
|
||||
) # Remove the last character and start a new line
|
||||
current_line = char # Start new line with the current character
|
||||
|
||||
# Add the last line
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
# Join all lines with newlines
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_tor_paths(self):
|
||||
if self.common.platform == "Linux":
|
||||
base_path = self.get_resource_path("tor")
|
||||
@ -595,6 +632,7 @@ class GuiCommon:
|
||||
if type(e) is WaitressException:
|
||||
return strings._("waitress_web_server_error")
|
||||
|
||||
|
||||
class ToggleCheckbox(QtWidgets.QCheckBox):
|
||||
def __init__(self, text):
|
||||
super(ToggleCheckbox, self).__init__(text)
|
||||
|
@ -18,6 +18,8 @@
|
||||
"gui_share_stop_server_autostop_timer": "Stop Sharing ({})",
|
||||
"gui_chat_start_server": "Start chat server",
|
||||
"gui_chat_stop_server": "Stop chat server",
|
||||
"gui_download_start_server": "Start downloading",
|
||||
"gui_download_stop_server": "Stop downloading",
|
||||
"gui_stop_server_autostop_timer_tooltip": "Auto-stop timer ends at {}",
|
||||
"gui_start_server_autostart_timer_tooltip": "Auto-start timer ends at {}",
|
||||
"gui_receive_start_server": "Start Receive Mode",
|
||||
@ -144,6 +146,8 @@
|
||||
"gui_status_indicator_chat_working": "Starting…",
|
||||
"gui_status_indicator_chat_scheduled": "Scheduled…",
|
||||
"gui_status_indicator_chat_started": "Chatting",
|
||||
"gui_status_indicator_download_stopped": "Stopped",
|
||||
"gui_status_indicator_download_working": "Downloading…",
|
||||
"gui_file_info": "{} files, {}",
|
||||
"gui_file_info_single": "{} file, {}",
|
||||
"history_in_progress_tooltip": "{} in progress",
|
||||
@ -152,6 +156,9 @@
|
||||
"error_cannot_create_data_dir": "Could not create OnionShare data folder: {}",
|
||||
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.</b>",
|
||||
"gui_chat_mode_explainer": "Chat mode lets you chat interactively with others, in Tor Browser.<br><br><b>Chat history is not stored in OnionShare. The chat history will disappear when you close Tor Browser.</b>",
|
||||
"gui_download_mode_explainer": "Has someone else used OnionShare to send you files?<br><br>You can enter the onion address here to download it, or use Tor Browser.",
|
||||
"gui_download_mode_in_progress": "You are downloading from {}",
|
||||
"gui_download_mode_in_progress_polling": "You are downloading from {}, every {} minutes",
|
||||
"gui_open_folder_error": "Could not open the folder with xdg-open. The file is here: {}",
|
||||
"gui_settings_language_label": "Language",
|
||||
"gui_settings_theme_label": "Theme",
|
||||
@ -192,20 +199,24 @@
|
||||
"gui_new_tab_receive_button": "Receive Files",
|
||||
"gui_new_tab_website_button": "Host a Website",
|
||||
"gui_new_tab_chat_button": "Chat Anonymously",
|
||||
"gui_new_tab_download_button": "Download from another OnionShare",
|
||||
"gui_main_page_share_button": "Start Sharing",
|
||||
"gui_main_page_receive_button": "Start Receiving",
|
||||
"gui_main_page_website_button": "Start Hosting",
|
||||
"gui_main_page_chat_button": "Start Chatting",
|
||||
"gui_main_page_download_button": "Start Downloading",
|
||||
"gui_tab_name_share": "Share",
|
||||
"gui_tab_name_receive": "Receive",
|
||||
"gui_tab_name_website": "Website",
|
||||
"gui_tab_name_chat": "Chat",
|
||||
"gui_tab_name_download": "Download",
|
||||
"gui_close_tab_warning_title": "Close tab?",
|
||||
"gui_close_tab_warning_persistent_description": "Close persistent tab and lose the onion address it is using?",
|
||||
"gui_close_tab_warning_share_description": "Close tab that is sending files?",
|
||||
"gui_close_tab_warning_receive_description": "Close tab that is receiving files?",
|
||||
"gui_close_tab_warning_chat_description": "Close tab that is hosting a chat server?",
|
||||
"gui_close_tab_warning_website_description": "Close tab that is hosting a website?",
|
||||
"gui_close_tab_warning_download_description": "Close tab that is downloading a share?",
|
||||
"gui_close_tab_warning_close": "Ok",
|
||||
"gui_close_tab_warning_cancel": "Cancel",
|
||||
"gui_quit_warning_title": "Quit OnionShare?",
|
||||
@ -228,6 +239,11 @@
|
||||
"mode_settings_receive_webhook_url_checkbox": "Use notification webhook",
|
||||
"mode_settings_website_disable_csp_checkbox": "Don't send default Content Security Policy header (allows your website to use third-party resources)",
|
||||
"mode_settings_website_custom_csp_checkbox": "Send a custom Content Security Policy header",
|
||||
"mode_settings_download_onionshare_url_label": "Enter the OnionShare URL that you were sent:",
|
||||
"mode_settings_download_onionshare_uses_private_key": "Were you also sent a Private Key?",
|
||||
"mode_settings_download_onionshare_private_key": "Enter the private key here",
|
||||
"mode_settings_download_poll_label": "Poll the OnionShare URL to download the share periodically (in minutes)",
|
||||
"mode_settings_download_poll_warning_label": "<b>Note: polling will overwrite the same downloaded file each time.</b>",
|
||||
"gui_all_modes_transfer_finished_range": "Transferred {} - {}",
|
||||
"gui_all_modes_transfer_finished": "Transferred {}",
|
||||
"gui_all_modes_transfer_canceled_range": "Canceled {} - {}",
|
||||
@ -246,6 +262,7 @@
|
||||
"gui_rendezvous_cleanup_quit_early": "Quit Early",
|
||||
"error_port_not_available": "OnionShare port not available",
|
||||
"history_receive_read_message_button": "Read Message",
|
||||
"history_downloads_error": "There was an error downloading: {}",
|
||||
"error_tor_protocol_error": "There was an error with Tor: {}",
|
||||
"error_generic": "There was an unexpected error with OnionShare:\n{}",
|
||||
"moat_contact_label": "Contacting BridgeDB…",
|
||||
|
@ -60,7 +60,8 @@ class Mode(QtWidgets.QWidget):
|
||||
|
||||
self.filenames = tab.filenames
|
||||
|
||||
# The web object gets created in init()
|
||||
# The web object gets created in init() for modes that use a server
|
||||
self.is_server = True
|
||||
self.web = None
|
||||
|
||||
# Threads start out as None
|
||||
@ -438,7 +439,8 @@ class Mode(QtWidgets.QWidget):
|
||||
except Exception:
|
||||
# Probably we had no port to begin with (Onion service didn't start)
|
||||
pass
|
||||
self.web.cleanup()
|
||||
if self.is_server:
|
||||
self.web.cleanup()
|
||||
|
||||
self.stop_server_custom()
|
||||
|
||||
@ -568,7 +570,9 @@ class Mode(QtWidgets.QWidget):
|
||||
self.primary_action.show()
|
||||
if not self.tab.timer.isActive():
|
||||
self.tab.timer.start(500)
|
||||
if self.settings.get("persistent", "enabled") and self.settings.get("persistent", "autostart_on_launch"):
|
||||
if self.settings.get("persistent", "enabled") and self.settings.get(
|
||||
"persistent", "autostart_on_launch"
|
||||
):
|
||||
self.server_status.start_server()
|
||||
|
||||
def tor_connection_stopped(self):
|
||||
|
500
desktop/onionshare/tab/mode/download_mode/__init__.py
Normal file
500
desktop/onionshare/tab/mode/download_mode/__init__.py
Normal file
@ -0,0 +1,500 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2014-2022 Micah Lee, et al. <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 os
|
||||
import re
|
||||
import requests
|
||||
|
||||
from PySide6 import QtCore, QtWidgets, QtGui
|
||||
|
||||
from onionshare_cli.common import Common
|
||||
from onionshare_cli.web import Web
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import Mode
|
||||
from ..history import History, ToggleHistory, DownloadHistoryItem
|
||||
from .... import strings
|
||||
from ....threads import DownloadThread
|
||||
from ....widgets import MinimumSizeWidget
|
||||
from ....gui_common import GuiCommon
|
||||
|
||||
|
||||
class DownloadMode(Mode):
|
||||
"""
|
||||
Parts of the main window UI for downloading a share from another OnionShare.
|
||||
"""
|
||||
|
||||
success = QtCore.Signal()
|
||||
error = QtCore.Signal(str)
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
Custom initialization for DownloadMode.
|
||||
"""
|
||||
|
||||
self.id = 0
|
||||
|
||||
# Download mode is client-only (no web server)
|
||||
self.is_server = False
|
||||
# Polling sets this to true and prevents the mode from 'stopping'
|
||||
self.is_polling = False
|
||||
|
||||
# Download (receive) image
|
||||
self.image_label = QtWidgets.QLabel()
|
||||
self.image_label.setPixmap(
|
||||
QtGui.QPixmap.fromImage(
|
||||
QtGui.QImage(
|
||||
GuiCommon.get_resource_path(
|
||||
"images/{}_mode_receive.png".format(self.common.gui.color_mode)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.image_label.setFixedSize(300, 300)
|
||||
image_layout = QtWidgets.QVBoxLayout()
|
||||
image_layout.addStretch()
|
||||
image_layout.addWidget(self.image_label)
|
||||
image_layout.addStretch()
|
||||
self.image = QtWidgets.QWidget()
|
||||
self.image.setLayout(image_layout)
|
||||
|
||||
# Header
|
||||
header_label = QtWidgets.QLabel(strings._("gui_new_tab_download_button"))
|
||||
header_label.setWordWrap(True)
|
||||
header_label.setStyleSheet(self.common.gui.css["mode_header_label"])
|
||||
|
||||
# Download mode explainer
|
||||
self.download_mode_explainer = QtWidgets.QLabel(
|
||||
strings._("gui_download_mode_explainer")
|
||||
)
|
||||
self.download_mode_explainer.setMinimumHeight(80)
|
||||
self.download_mode_explainer.setWordWrap(True)
|
||||
|
||||
# OnionShare URL
|
||||
self.onionshare_url_label = QtWidgets.QLabel(
|
||||
strings._("mode_settings_download_onionshare_url_label")
|
||||
)
|
||||
self.onionshare_url = QtWidgets.QLineEdit()
|
||||
self.onionshare_url.setStyleSheet(
|
||||
"QLineEdit { color: black; } QLineEdit::placeholder { color: gray; }"
|
||||
)
|
||||
self.onionshare_url.setPlaceholderText(
|
||||
"http://lldan5gahapx5k7iafb3s4ikijc4ni7gx5iywdflkba5y2ezyg6sjgyd.onion"
|
||||
)
|
||||
|
||||
# Does the OnionShare URL use Client Auth?
|
||||
self.onionshare_uses_private_key_checkbox = QtWidgets.QCheckBox()
|
||||
self.onionshare_uses_private_key_checkbox.setText(
|
||||
strings._("mode_settings_download_onionshare_uses_private_key")
|
||||
)
|
||||
self.onionshare_uses_private_key_checkbox.clicked.connect(
|
||||
self.onionshare_uses_private_key_checkbox_checked
|
||||
)
|
||||
|
||||
self.onionshare_private_key = QtWidgets.QLineEdit()
|
||||
self.onionshare_private_key.setPlaceholderText(
|
||||
strings._("mode_settings_download_onionshare_private_key")
|
||||
)
|
||||
self.onionshare_private_key.setStyleSheet(
|
||||
"QLineEdit { color: black; } QLineEdit::placeholder { color: gray; }"
|
||||
)
|
||||
self.onionshare_private_key.hide()
|
||||
|
||||
onionshare_url_layout = QtWidgets.QVBoxLayout()
|
||||
onionshare_url_layout.setContentsMargins(0, 0, 0, 0)
|
||||
onionshare_url_layout.addWidget(self.onionshare_url_label)
|
||||
onionshare_url_layout.addWidget(self.onionshare_url)
|
||||
onionshare_url_layout.addWidget(self.onionshare_uses_private_key_checkbox)
|
||||
onionshare_url_layout.addWidget(self.onionshare_private_key)
|
||||
self.mode_settings_widget.mode_specific_layout.addLayout(onionshare_url_layout)
|
||||
|
||||
# Polling option
|
||||
self.poll_checkbox = QtWidgets.QCheckBox()
|
||||
self.poll_checkbox.setText(strings._("mode_settings_download_poll_label"))
|
||||
self.poll_checkbox.clicked.connect(self.poll_checkbox_checked)
|
||||
self.poll = QtWidgets.QSpinBox()
|
||||
self.poll.setRange(0, 525600)
|
||||
self.poll.setValue(0) # Set initial value to 0
|
||||
self.poll.setSingleStep(1) # increment in minutes
|
||||
self.poll.valueChanged.connect(self.poll_checkbox_checked)
|
||||
|
||||
self.poll_warning_label = QtWidgets.QLabel(
|
||||
strings._("mode_settings_download_poll_warning_label")
|
||||
)
|
||||
self.poll_warning_label.hide()
|
||||
poll_warning_layout = QtWidgets.QHBoxLayout()
|
||||
poll_warning_layout.addWidget(self.poll_warning_label)
|
||||
|
||||
poll_settings_layout = QtWidgets.QHBoxLayout()
|
||||
poll_settings_layout.addWidget(self.poll_checkbox)
|
||||
poll_settings_layout.addWidget(self.poll)
|
||||
|
||||
poll_layout = QtWidgets.QVBoxLayout()
|
||||
poll_layout.addLayout(poll_settings_layout)
|
||||
poll_layout.addLayout(poll_warning_layout)
|
||||
self.mode_settings_widget.mode_specific_layout.addLayout(poll_layout)
|
||||
|
||||
# Data dir
|
||||
data_dir_label = QtWidgets.QLabel(
|
||||
strings._("mode_settings_receive_data_dir_label")
|
||||
)
|
||||
self.data_dir_lineedit = QtWidgets.QLineEdit()
|
||||
self.data_dir_lineedit.setReadOnly(True)
|
||||
self.data_dir_lineedit.setText(self.settings.get("download", "data_dir"))
|
||||
data_dir_button = QtWidgets.QPushButton(
|
||||
strings._("mode_settings_receive_data_dir_browse_button")
|
||||
)
|
||||
data_dir_button.clicked.connect(self.data_dir_button_clicked)
|
||||
data_dir_layout = QtWidgets.QHBoxLayout()
|
||||
data_dir_layout.addWidget(data_dir_label)
|
||||
data_dir_layout.addWidget(self.data_dir_lineedit)
|
||||
data_dir_layout.addWidget(data_dir_button)
|
||||
self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout)
|
||||
|
||||
# These settings make no sense for Download mode
|
||||
self.mode_settings_widget.persistent_checkbox.hide()
|
||||
self.mode_settings_widget.persistent_autostart_on_launch_checkbox.hide()
|
||||
self.mode_settings_widget.public_checkbox.hide()
|
||||
self.mode_settings_widget.advanced_widget.hide()
|
||||
self.mode_settings_widget.toggle_advanced_button.hide()
|
||||
|
||||
# Server status
|
||||
self.server_status.set_mode("download")
|
||||
self.server_status.server_stopped.connect(self.update_primary_action)
|
||||
|
||||
self.server_status.update()
|
||||
|
||||
# Download history
|
||||
self.history = History(
|
||||
self.common,
|
||||
QtGui.QPixmap.fromImage(
|
||||
QtGui.QImage(
|
||||
GuiCommon.get_resource_path("images/share_icon_transparent.png")
|
||||
)
|
||||
),
|
||||
strings._("gui_website_mode_no_files"),
|
||||
strings._("gui_all_modes_history"),
|
||||
"download",
|
||||
)
|
||||
self.history.requests_label.hide()
|
||||
self.history.hide()
|
||||
|
||||
# Info label
|
||||
self.info_label = QtWidgets.QLabel()
|
||||
self.info_label.hide()
|
||||
|
||||
# Toggle history
|
||||
self.toggle_history = ToggleHistory(
|
||||
self.common,
|
||||
self,
|
||||
self.history,
|
||||
QtGui.QIcon(
|
||||
GuiCommon.get_resource_path(
|
||||
f"images/{self.common.gui.color_mode}_history_icon_toggle.svg"
|
||||
)
|
||||
),
|
||||
QtGui.QIcon(
|
||||
GuiCommon.get_resource_path(
|
||||
f"images/{self.common.gui.color_mode}_history_icon_toggle_selected.svg"
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Top bar
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
# Add space at the top, same height as the toggle history bar in other modes
|
||||
top_bar_layout.addWidget(MinimumSizeWidget(0, 30))
|
||||
top_bar_layout.addWidget(self.info_label)
|
||||
top_bar_layout.addStretch()
|
||||
top_bar_layout.addWidget(self.toggle_history)
|
||||
|
||||
# Primary action layout
|
||||
self.primary_action.hide()
|
||||
self.update_primary_action()
|
||||
|
||||
# Main layout
|
||||
self.main_layout = QtWidgets.QVBoxLayout()
|
||||
self.main_layout.addLayout(top_bar_layout)
|
||||
self.main_layout.addWidget(header_label)
|
||||
self.main_layout.addWidget(self.download_mode_explainer)
|
||||
self.main_layout.addWidget(self.primary_action, stretch=1)
|
||||
self.main_layout.addWidget(self.server_status)
|
||||
self.main_layout.addWidget(MinimumSizeWidget(700, 0))
|
||||
|
||||
# Column layout
|
||||
self.column_layout = QtWidgets.QHBoxLayout()
|
||||
self.column_layout.addWidget(self.image)
|
||||
self.column_layout.addLayout(self.main_layout)
|
||||
self.column_layout.addWidget(self.history, stretch=1)
|
||||
|
||||
# Content layout
|
||||
self.content_layout.addLayout(self.column_layout)
|
||||
|
||||
def get_type(self):
|
||||
"""
|
||||
Returns the type of mode as a string (e.g. "share", "receive", etc.)
|
||||
"""
|
||||
return "download"
|
||||
|
||||
def onionshare_uses_private_key_checkbox_checked(self):
|
||||
"""
|
||||
If the user checked the box saying a Private Key was sent, show the private key lineEdit field.
|
||||
"""
|
||||
if self.onionshare_uses_private_key_checkbox.isChecked():
|
||||
self.onionshare_private_key.show()
|
||||
else:
|
||||
self.onionshare_private_key.hide()
|
||||
|
||||
def data_dir_button_clicked(self):
|
||||
"""
|
||||
Browse for a new OnionShare data directory, and save to tab settings
|
||||
"""
|
||||
data_dir = self.data_dir_lineedit.text()
|
||||
selected_dir = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self, strings._("mode_settings_receive_data_dir_label"), data_dir
|
||||
)
|
||||
|
||||
if selected_dir:
|
||||
# If we're running inside a flatpak package, the data dir must be inside ~/OnionShare
|
||||
if self.common.gui.is_flatpak:
|
||||
if not selected_dir.startswith(os.path.expanduser("~/OnionShare")):
|
||||
Alert(self.common, strings._("gui_receive_flatpak_data_dir"))
|
||||
return
|
||||
|
||||
self.common.log(
|
||||
"DownloadMode",
|
||||
"data_dir_button_clicked",
|
||||
f"selected dir: {selected_dir}",
|
||||
)
|
||||
self.data_dir_lineedit.setText(selected_dir)
|
||||
self.settings.set("download", "data_dir", selected_dir)
|
||||
|
||||
def poll_checkbox_checked(self):
|
||||
"""
|
||||
If the user checks the poll option or changes the value of the poll interval
|
||||
when it is checkced, set the poll interval in settings.
|
||||
"""
|
||||
if self.poll_checkbox.isChecked():
|
||||
self.common.log(
|
||||
"DownloadMode",
|
||||
"poll_checkbox_checked",
|
||||
f"Set poll to : {self.poll.value()}",
|
||||
)
|
||||
self.settings.set("download", "poll", self.poll.value())
|
||||
self.poll_warning_label.show()
|
||||
else:
|
||||
self.settings.set("download", "poll", 0)
|
||||
self.poll_warning_label.hide()
|
||||
|
||||
def extract_domain(self, url):
|
||||
# First, parse the URL to get the domain
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
# Extract the domain (without scheme and path)
|
||||
domain_with_tld = parsed_url.netloc.split(":")[0] # Remove port if any
|
||||
|
||||
# Regular expression to extract the main domain name
|
||||
match = re.search(r"([a-zA-Z0-9-]+)\.[a-zA-Z]{2,}", domain_with_tld)
|
||||
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
return None # Return None if no domain is found
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the onionshare server. This uses multiple threads to start the Tor onion
|
||||
server and the web app.
|
||||
"""
|
||||
self.common.log("Mode", "start_server")
|
||||
self.id += 1
|
||||
|
||||
self.set_server_active.emit(True)
|
||||
|
||||
# Hide and reset the downloads if we have previously shared
|
||||
self.reset_info_counters()
|
||||
|
||||
# Clear the status bar
|
||||
self.status_bar.clearMessage()
|
||||
self.server_status_label.setText("")
|
||||
|
||||
# Hide the mode settings
|
||||
self.mode_settings_widget.hide()
|
||||
|
||||
self.service_id = self.extract_domain(self.onionshare_url.text().strip())
|
||||
|
||||
self.common.log("DownloadMode", "start_server")
|
||||
|
||||
if not self.common.gui.local_only:
|
||||
self.download_thread = DownloadThread(self)
|
||||
|
||||
self.download_thread.begun.connect(self.add_download_item)
|
||||
self.download_thread.progress.connect(self.progress_download_item)
|
||||
self.download_thread.success.connect(self.finished_download_item)
|
||||
self.download_thread.error.connect(self.error_download_item)
|
||||
self.download_thread.locked.connect(self.locked_download_item)
|
||||
|
||||
# Start the download thread
|
||||
self.download_thread.start()
|
||||
|
||||
if (
|
||||
self.settings.get("download", "poll")
|
||||
and self.settings.get("download", "poll") >= 1
|
||||
):
|
||||
self.is_polling = True
|
||||
# Set up a QTimer to trigger the thread at end of each polling interval
|
||||
self.timer = QtCore.QTimer(self)
|
||||
self.timer.timeout.connect(self.trigger_download_thread)
|
||||
self.timer.setInterval(
|
||||
self.settings.get("download", "poll") * 60 * 1000
|
||||
) # Minutes to milliseconds
|
||||
self.timer.start()
|
||||
|
||||
# Update the 'Explainer' label to explain what is happening.
|
||||
if self.is_polling:
|
||||
self.download_mode_explainer.setText(
|
||||
strings._("gui_download_mode_in_progress_polling").format(
|
||||
self.onionshare_url.text().strip(), self.poll.value()
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.download_mode_explainer.setText(
|
||||
strings._("gui_download_mode_in_progress").format(
|
||||
self.onionshare_url.text().strip()
|
||||
)
|
||||
)
|
||||
|
||||
def trigger_download_thread(self):
|
||||
if not self.download_thread._lock:
|
||||
self.download_thread.start()
|
||||
|
||||
def start_server_step2_custom(self):
|
||||
# Continue
|
||||
self.starting_server_step3.emit()
|
||||
self.start_server_finished.emit()
|
||||
|
||||
def cancel_server_custom(self):
|
||||
"""
|
||||
Log that the server has been cancelled
|
||||
"""
|
||||
self.common.log("DownloadMode", "cancel_server")
|
||||
|
||||
def stop_server_custom(self):
|
||||
"""
|
||||
If any polling is taking place, stop iot
|
||||
"""
|
||||
if self.is_polling:
|
||||
self.common.log(
|
||||
"DownloadMode", "stop_server_custom", "Stopping Download polling timer"
|
||||
)
|
||||
self.timer.stop()
|
||||
if self.download_thread:
|
||||
self.common.log(
|
||||
"DownloadMode", "stop_server_custom", "Stopping DownloadThread"
|
||||
)
|
||||
self.download_thread.quit()
|
||||
self.download_thread.wait()
|
||||
|
||||
self.common.gui.onion.remove_onion_client_auth(self.service_id)
|
||||
self.download_mode_explainer.setText(strings._("gui_download_mode_explainer"))
|
||||
|
||||
def handle_tor_broke_custom(self):
|
||||
"""
|
||||
Connection to Tor broke.
|
||||
"""
|
||||
self.primary_action.hide()
|
||||
|
||||
def update_primary_action(self):
|
||||
self.common.log("DownloadMode", "update_primary_action")
|
||||
|
||||
# Show or hide primary action layout
|
||||
self.primary_action.show()
|
||||
self.info_label.show()
|
||||
|
||||
def add_download_item(self):
|
||||
self.common.log("DownloadMode", "add_download_item", self.id)
|
||||
item = DownloadHistoryItem(
|
||||
self.common,
|
||||
self.id,
|
||||
self.onionshare_url.text().strip(),
|
||||
)
|
||||
|
||||
self.history.add(self.id, item)
|
||||
self.toggle_history.update_indicator(True)
|
||||
self.history.in_progress_count += 1
|
||||
self.history.update_in_progress()
|
||||
|
||||
def progress_download_item(self, progress):
|
||||
self.common.log(
|
||||
"DownloadMode", "progress_download_item", f"Progress: {progress}%"
|
||||
)
|
||||
self.history.update(self.id, {"action": "progress", "progress": progress})
|
||||
self.history.update_in_progress()
|
||||
|
||||
def finished_download_item(self, file_path, file_name, file_size):
|
||||
self.common.log(
|
||||
"DownloadMode",
|
||||
"finished_download_item",
|
||||
f"File path is {file_path}, file name is {file_name}, file size is {file_size}",
|
||||
)
|
||||
self.history.update(
|
||||
self.id,
|
||||
{
|
||||
"action": "finished",
|
||||
"file_path": file_path,
|
||||
"file_name": file_name,
|
||||
"file_size": file_size,
|
||||
},
|
||||
)
|
||||
self.history.completed_count += 1
|
||||
self.history.in_progress_count -= 1
|
||||
self.history.update_completed()
|
||||
self.history.update_in_progress()
|
||||
|
||||
self.start_server_step2_custom()
|
||||
if not self.is_polling:
|
||||
self.stop_server()
|
||||
|
||||
def locked_download_item(self):
|
||||
self.common.log(
|
||||
"DownloadMode",
|
||||
"locked_download_item",
|
||||
"Thread is locked, a download must be running",
|
||||
)
|
||||
if self.history.in_progress_count > 0:
|
||||
self.history.in_progress_count -= 1
|
||||
|
||||
def error_download_item(self, error):
|
||||
self.common.log("DownloadMode", "error_download_item", f"{self.id}: {error}")
|
||||
self.history.update(self.id, {"action": "error", "error": error})
|
||||
if self.history.in_progress_count > 0:
|
||||
self.history.in_progress_count -= 1
|
||||
self.history.update_in_progress()
|
||||
if not self.is_polling:
|
||||
self.stop_server()
|
||||
|
||||
def reset_info_counters(self):
|
||||
"""
|
||||
Set the info counters back to zero.
|
||||
"""
|
||||
self.history.reset()
|
||||
self.toggle_history.indicator_count = 0
|
||||
self.toggle_history.update_indicator()
|
@ -192,8 +192,11 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
|
||||
self.started = datetime.now()
|
||||
|
||||
# Filename label
|
||||
self.filename_label = QtWidgets.QLabel(self.filename)
|
||||
self.filename_label_width = self.filename_label.width()
|
||||
self.filename_label = QtWidgets.QLabel()
|
||||
self.filename_label.setWordWrap(True)
|
||||
self.filename_label.setMaximumWidth(250)
|
||||
filename_text = self.common.gui.wrap_text(self.filename_label, self.filename)
|
||||
self.filename_label.setText(filename_text)
|
||||
|
||||
# File size label
|
||||
self.filesize_label = QtWidgets.QLabel()
|
||||
@ -229,7 +232,8 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
|
||||
|
||||
def rename(self, new_filename):
|
||||
self.filename = new_filename
|
||||
self.filename_label.setText(self.filename)
|
||||
filename_text = self.common.gui.wrap_text(self.filename_label, self.filename)
|
||||
self.filename_label.setText(filename_text)
|
||||
|
||||
def set_dir(self, dir):
|
||||
self.dir = dir
|
||||
@ -483,9 +487,14 @@ class IndividualFileHistoryItem(HistoryItem):
|
||||
self.timestamp_label.setStyleSheet(
|
||||
self.common.gui.css["history_individual_file_timestamp_label"]
|
||||
)
|
||||
self.path_label = QtWidgets.QLabel(self.path)
|
||||
self.path_label = QtWidgets.QLabel()
|
||||
self.path_label.setTextFormat(QtCore.Qt.PlainText)
|
||||
self.path_label.setStyleSheet(self.common.gui.css["history_default_label"])
|
||||
self.path_label.setWordWrap(True)
|
||||
self.path_label.setMaximumWidth(250)
|
||||
path_text = self.common.gui.wrap_text(self.path_label, self.path)
|
||||
self.path_label.setText(path_text)
|
||||
|
||||
self.status_code_label = QtWidgets.QLabel()
|
||||
|
||||
# Progress bar
|
||||
@ -788,10 +797,14 @@ class History(QtWidgets.QWidget):
|
||||
Update the 'completed' widget.
|
||||
"""
|
||||
if self.completed_count == 0:
|
||||
image = GuiCommon.get_resource_path(f"images/{self.common.gui.color_mode}_history_completed_none.svg")
|
||||
image = GuiCommon.get_resource_path(
|
||||
f"images/{self.common.gui.color_mode}_history_completed_none.svg"
|
||||
)
|
||||
else:
|
||||
image = GuiCommon.get_resource_path("images/history_completed.svg")
|
||||
self.completed_label.setText(f'<img src="{image}" height="10" /> {self.completed_count}')
|
||||
self.completed_label.setText(
|
||||
f'<img src="{image}" height="10" /> {self.completed_count}'
|
||||
)
|
||||
self.completed_label.setToolTip(
|
||||
strings._("history_completed_tooltip").format(self.completed_count)
|
||||
)
|
||||
@ -801,7 +814,9 @@ class History(QtWidgets.QWidget):
|
||||
Update the 'in progress' widget.
|
||||
"""
|
||||
if self.in_progress_count == 0:
|
||||
image = GuiCommon.get_resource_path(f"images/{self.common.gui.color_mode}_history_in_progress_none.svg")
|
||||
image = GuiCommon.get_resource_path(
|
||||
f"images/{self.common.gui.color_mode}_history_in_progress_none.svg"
|
||||
)
|
||||
else:
|
||||
image = GuiCommon.get_resource_path("images/history_in_progress.svg")
|
||||
|
||||
@ -817,11 +832,15 @@ class History(QtWidgets.QWidget):
|
||||
Update the 'web requests' widget.
|
||||
"""
|
||||
if self.requests_count == 0:
|
||||
image = GuiCommon.get_resource_path(f"images/{self.common.gui.color_mode}_history_requests_none.svg")
|
||||
image = GuiCommon.get_resource_path(
|
||||
f"images/{self.common.gui.color_mode}_history_requests_none.svg"
|
||||
)
|
||||
else:
|
||||
image = GuiCommon.get_resource_path("images/history_requests.svg")
|
||||
|
||||
self.requests_label.setText(f'<img src="{image}" height="10" /> {self.requests_count}')
|
||||
self.requests_label.setText(
|
||||
f'<img src="{image}" height="10" /> {self.requests_count}'
|
||||
)
|
||||
self.requests_label.setToolTip(
|
||||
strings._("history_requests_tooltip").format(self.requests_count)
|
||||
)
|
||||
@ -894,3 +913,159 @@ class ToggleHistory(QtWidgets.QPushButton):
|
||||
# Reset the indicator count
|
||||
self.indicator_count = 0
|
||||
self.update_indicator()
|
||||
|
||||
|
||||
class DownloadHistoryItem(HistoryItem):
|
||||
def __init__(self, common, id, url, file_name=None, file_size=None):
|
||||
super(DownloadHistoryItem, self).__init__()
|
||||
self.common = common
|
||||
self.id = id
|
||||
self.url = url
|
||||
self.file_name = file_name
|
||||
self.file_size = file_size
|
||||
self.started = datetime.now()
|
||||
self.status = HistoryItem.STATUS_STARTED
|
||||
|
||||
self.common.log(
|
||||
"DownloadHistoryItem",
|
||||
"__init__",
|
||||
f"id={self.id} url={self.url} file_name = {self.file_size} file_size = {self.file_size}",
|
||||
)
|
||||
|
||||
# Label
|
||||
self.label = QtWidgets.QLabel(
|
||||
strings._("gui_all_modes_transfer_started").format(
|
||||
self.started.strftime("%b %d, %I:%M%p")
|
||||
)
|
||||
)
|
||||
self.label.setWordWrap(True)
|
||||
|
||||
# URL so we know which onion service we downloaded this share from
|
||||
self.url_label = QtWidgets.QLabel()
|
||||
self.url_label.setWordWrap(True)
|
||||
self.url_label.setMaximumWidth(250)
|
||||
self.url_text = self.common.gui.wrap_text(self.url_label, self.url)
|
||||
self.url_label.setText(self.url_text)
|
||||
|
||||
# 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.setRange(0, 0)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setStyleSheet(
|
||||
self.common.gui.css["downloads_uploads_progress_bar"]
|
||||
)
|
||||
|
||||
# This layout contains file widgets
|
||||
self.files_layout = QtWidgets.QVBoxLayout()
|
||||
self.files_layout.setContentsMargins(0, 0, 0, 0)
|
||||
files_widget = QtWidgets.QWidget()
|
||||
files_widget.setStyleSheet(self.common.gui.css["receive_file"])
|
||||
files_widget.setLayout(self.files_layout)
|
||||
|
||||
# Layout
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.url_label)
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addWidget(files_widget)
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def update(self, data):
|
||||
"""
|
||||
Using the progress from Web, update the progress bar and file size labels
|
||||
for each file
|
||||
"""
|
||||
if data["action"] == "progress":
|
||||
# Update the progress bar
|
||||
self.progress_bar.setMaximum(100)
|
||||
self.progress_bar.setValue(data["progress"])
|
||||
|
||||
elif data["action"] == "finished":
|
||||
# Change the status
|
||||
self.status = HistoryItem.STATUS_FINISHED
|
||||
|
||||
# Hide the progress bar
|
||||
self.progress_bar.hide()
|
||||
|
||||
# Change the label
|
||||
self.label.setText(self.get_finished_label_text(self.started))
|
||||
self.label.setStyleSheet(self.common.gui.css["history_default_label"])
|
||||
|
||||
self.file_name = data["file_name"]
|
||||
self.file_path = data["file_path"]
|
||||
self.file_size = self.common.human_readable_filesize(data["file_size"])
|
||||
|
||||
# Filename label
|
||||
self.filename_label = QtWidgets.QLabel(self.file_name)
|
||||
self.filename_label_width = self.filename_label.width()
|
||||
|
||||
# File size label
|
||||
self.filesize_label = QtWidgets.QLabel(self.file_size)
|
||||
self.filesize_label.setStyleSheet(self.common.gui.css["receive_file_size"])
|
||||
|
||||
# Folder button
|
||||
image = QtGui.QImage(GuiCommon.get_resource_path("images/open_folder.svg"))
|
||||
scaled_image = image.scaledToHeight(15, QtCore.Qt.SmoothTransformation)
|
||||
folder_pixmap = QtGui.QPixmap.fromImage(scaled_image)
|
||||
folder_icon = QtGui.QIcon(folder_pixmap)
|
||||
self.folder_button = QtWidgets.QPushButton()
|
||||
self.folder_button.clicked.connect(self.open_folder)
|
||||
self.folder_button.setIcon(folder_icon)
|
||||
self.folder_button.setIconSize(folder_pixmap.rect().size())
|
||||
self.folder_button.setFlat(True)
|
||||
|
||||
# Layouts
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addWidget(self.filename_label)
|
||||
layout.addWidget(self.filesize_label)
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.folder_button)
|
||||
|
||||
file_widget = QtWidgets.QWidget()
|
||||
file_widget.setLayout(layout)
|
||||
self.files_layout.addWidget(file_widget)
|
||||
|
||||
if data["action"] == "error":
|
||||
# Change the status
|
||||
self.status = HistoryItem.STATUS_CANCELED
|
||||
|
||||
# Hide the progress bar
|
||||
self.progress_bar.hide()
|
||||
|
||||
# Change the label
|
||||
error_data_formatted = data["error"]
|
||||
self.error_data_text = self.common.gui.wrap_text(self.label, data["error"])
|
||||
self.label.setText(
|
||||
strings._("history_downloads_error").format(self.error_data_text)
|
||||
)
|
||||
self.label.setStyleSheet(self.common.gui.css["history_default_label"])
|
||||
|
||||
def open_folder(self):
|
||||
"""
|
||||
Open the downloads folder, with the file selected, in a cross-platform manner
|
||||
"""
|
||||
self.common.log("DownloadHistoryItemFile", "open_folder")
|
||||
|
||||
# Linux
|
||||
if self.common.platform == "Linux" or self.common.platform == "BSD":
|
||||
try:
|
||||
# If nautilus is available, open it
|
||||
subprocess.Popen(["xdg-open", self.file_path])
|
||||
except Exception:
|
||||
Alert(
|
||||
self.common,
|
||||
strings._("gui_open_folder_error").format(self.file_path),
|
||||
)
|
||||
|
||||
# macOS
|
||||
elif self.common.platform == "Darwin":
|
||||
subprocess.call(["open", "-R", self.file_path])
|
||||
|
||||
# Windows
|
||||
elif self.common.platform == "Windows":
|
||||
subprocess.Popen(["explorer", f"/select,{self.file_path}"])
|
||||
|
@ -40,12 +40,20 @@ class ModeSettingsWidget(QtWidgets.QScrollArea):
|
||||
self.mode_specific_layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self.persistent_autostart_on_launch_checkbox = QtWidgets.QCheckBox()
|
||||
self.persistent_autostart_on_launch_checkbox.clicked.connect(self.persistent_autostart_on_launch_checkbox_clicked)
|
||||
self.persistent_autostart_on_launch_checkbox.setText(strings._("mode_settings_persistent_autostart_on_launch_checkbox"))
|
||||
self.persistent_autostart_on_launch_checkbox.clicked.connect(
|
||||
self.persistent_autostart_on_launch_checkbox_clicked
|
||||
)
|
||||
self.persistent_autostart_on_launch_checkbox.setText(
|
||||
strings._("mode_settings_persistent_autostart_on_launch_checkbox")
|
||||
)
|
||||
if self.settings.get("persistent", "autostart_on_launch"):
|
||||
self.persistent_autostart_on_launch_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||
self.persistent_autostart_on_launch_checkbox.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
)
|
||||
else:
|
||||
self.persistent_autostart_on_launch_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.persistent_autostart_on_launch_checkbox.setCheckState(
|
||||
QtCore.Qt.Unchecked
|
||||
)
|
||||
|
||||
# Persistent
|
||||
self.persistent_checkbox = QtWidgets.QCheckBox()
|
||||
@ -207,6 +215,10 @@ class ModeSettingsWidget(QtWidgets.QScrollArea):
|
||||
self.tab.change_title.emit(
|
||||
self.tab.tab_id, strings._("gui_tab_name_chat")
|
||||
)
|
||||
elif self.tab.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
self.tab.change_title.emit(
|
||||
self.tab.tab_id, strings._("gui_tab_name_download")
|
||||
)
|
||||
elif self.tab_mode is None:
|
||||
pass
|
||||
else:
|
||||
@ -217,7 +229,11 @@ class ModeSettingsWidget(QtWidgets.QScrollArea):
|
||||
def persistent_checkbox_clicked(self):
|
||||
self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked())
|
||||
self.settings.set("persistent", "mode", self.tab.mode)
|
||||
self.settings.set("persistent", "autostart_on_launch", self.persistent_autostart_on_launch_checkbox.isChecked())
|
||||
self.settings.set(
|
||||
"persistent",
|
||||
"autostart_on_launch",
|
||||
self.persistent_autostart_on_launch_checkbox.isChecked(),
|
||||
)
|
||||
self.change_persistent.emit(
|
||||
self.tab.tab_id, self.persistent_checkbox.isChecked()
|
||||
)
|
||||
@ -230,7 +246,11 @@ class ModeSettingsWidget(QtWidgets.QScrollArea):
|
||||
self.persistent_autostart_on_launch_checkbox.show()
|
||||
|
||||
def persistent_autostart_on_launch_checkbox_clicked(self):
|
||||
self.settings.set("persistent", "autostart_on_launch", self.persistent_autostart_on_launch_checkbox.isChecked())
|
||||
self.settings.set(
|
||||
"persistent",
|
||||
"autostart_on_launch",
|
||||
self.persistent_autostart_on_launch_checkbox.isChecked(),
|
||||
)
|
||||
|
||||
def public_checkbox_clicked(self):
|
||||
self.settings.set("general", "public", self.public_checkbox.isChecked())
|
||||
|
@ -326,7 +326,7 @@ class ServerStatus(QtWidgets.QWidget):
|
||||
"""
|
||||
self.common.log("ServerStatus", "update")
|
||||
# Set the URL fields
|
||||
if self.status == self.STATUS_STARTED:
|
||||
if self.status == self.STATUS_STARTED and not self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
# The backend Onion may have saved new settings, such as the private key.
|
||||
# Reload the settings before saving new ones.
|
||||
self.common.settings.load()
|
||||
@ -379,6 +379,8 @@ class ServerStatus(QtWidgets.QWidget):
|
||||
self.server_button.setText(strings._("gui_share_start_server"))
|
||||
elif self.mode == self.common.gui.MODE_CHAT:
|
||||
self.server_button.setText(strings._("gui_chat_start_server"))
|
||||
elif self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
self.server_button.setText(strings._("gui_download_start_server"))
|
||||
else:
|
||||
self.server_button.setText(strings._("gui_receive_start_server"))
|
||||
self.server_button.setToolTip("")
|
||||
@ -393,6 +395,8 @@ class ServerStatus(QtWidgets.QWidget):
|
||||
self.server_button.setText(strings._("gui_share_stop_server"))
|
||||
elif self.mode == self.common.gui.MODE_CHAT:
|
||||
self.server_button.setText(strings._("gui_chat_stop_server"))
|
||||
elif self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
self.server_button.setText(strings._("gui_download_stop_server"))
|
||||
else:
|
||||
self.server_button.setText(strings._("gui_receive_stop_server"))
|
||||
elif self.status == self.STATUS_WORKING:
|
||||
@ -409,12 +413,18 @@ class ServerStatus(QtWidgets.QWidget):
|
||||
)
|
||||
)
|
||||
else:
|
||||
if self.common.platform == "Windows":
|
||||
self.server_button.setText(strings._("gui_please_wait"))
|
||||
else:
|
||||
if self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
self.server_button.setEnabled(True)
|
||||
self.server_button.setText(
|
||||
strings._("gui_please_wait_no_button")
|
||||
strings._("gui_status_indicator_download_working")
|
||||
)
|
||||
else:
|
||||
if self.common.platform == "Windows":
|
||||
self.server_button.setText(strings._("gui_please_wait"))
|
||||
else:
|
||||
self.server_button.setText(
|
||||
strings._("gui_please_wait_no_button")
|
||||
)
|
||||
else:
|
||||
self.server_button.setStyleSheet(
|
||||
self.common.gui.css["server_status_button_working"]
|
||||
|
@ -29,6 +29,7 @@ from .mode.share_mode import ShareMode
|
||||
from .mode.receive_mode import ReceiveMode
|
||||
from .mode.website_mode import WebsiteMode
|
||||
from .mode.chat_mode import ChatMode
|
||||
from .mode.download_mode import DownloadMode
|
||||
|
||||
from .server_status import ServerStatus
|
||||
|
||||
@ -133,11 +134,10 @@ class Tab(QtWidgets.QWidget):
|
||||
)
|
||||
)
|
||||
)
|
||||
self.image_label.setFixedSize(180, 40)
|
||||
image_layout = QtWidgets.QVBoxLayout()
|
||||
image_layout.addWidget(self.image_label)
|
||||
image_layout.addStretch()
|
||||
self.image = QtWidgets.QWidget()
|
||||
self.image.setFixedSize(280, 280)
|
||||
self.image.setLayout(image_layout)
|
||||
|
||||
# New tab buttons
|
||||
@ -177,14 +177,25 @@ class Tab(QtWidgets.QWidget):
|
||||
)
|
||||
self.chat_button.clicked.connect(self.chat_mode_clicked)
|
||||
|
||||
self.download_button = NewTabButton(
|
||||
self.common,
|
||||
"images/{}_mode_new_tab_receive.png".format(self.common.gui.color_mode),
|
||||
strings._("gui_new_tab_download_button"),
|
||||
strings._("gui_main_page_download_button"),
|
||||
QtCore.Qt.Key_D,
|
||||
)
|
||||
self.download_button.clicked.connect(self.download_mode_clicked)
|
||||
|
||||
new_tab_top_layout = QtWidgets.QHBoxLayout()
|
||||
new_tab_top_layout.addStretch()
|
||||
new_tab_top_layout.addWidget(self.image)
|
||||
new_tab_top_layout.addWidget(self.share_button)
|
||||
new_tab_top_layout.addWidget(self.receive_button)
|
||||
new_tab_top_layout.addStretch()
|
||||
|
||||
new_tab_bottom_layout = QtWidgets.QHBoxLayout()
|
||||
new_tab_bottom_layout.addStretch()
|
||||
new_tab_bottom_layout.addWidget(self.download_button)
|
||||
new_tab_bottom_layout.addWidget(self.website_button)
|
||||
new_tab_bottom_layout.addWidget(self.chat_button)
|
||||
new_tab_bottom_layout.addStretch()
|
||||
@ -195,14 +206,8 @@ class Tab(QtWidgets.QWidget):
|
||||
new_tab_layout.addLayout(new_tab_bottom_layout)
|
||||
new_tab_layout.addStretch()
|
||||
|
||||
new_tab_img_layout = QtWidgets.QHBoxLayout()
|
||||
new_tab_img_layout.addWidget(self.image)
|
||||
new_tab_img_layout.addStretch(1)
|
||||
new_tab_img_layout.addLayout(new_tab_layout)
|
||||
new_tab_img_layout.addStretch(2)
|
||||
|
||||
self.new_tab = QtWidgets.QWidget()
|
||||
self.new_tab.setLayout(new_tab_img_layout)
|
||||
self.new_tab.setLayout(new_tab_layout)
|
||||
self.new_tab.show()
|
||||
|
||||
# Layout
|
||||
@ -253,6 +258,8 @@ class Tab(QtWidgets.QWidget):
|
||||
self.website_mode_clicked()
|
||||
elif mode == "chat":
|
||||
self.chat_mode_clicked()
|
||||
elif mode == "download":
|
||||
self.download_mode_clicked()
|
||||
else:
|
||||
# This is a new tab
|
||||
self.settings = ModeSettings(self.common)
|
||||
@ -399,6 +406,38 @@ class Tab(QtWidgets.QWidget):
|
||||
self.update_server_status_indicator()
|
||||
self.timer.start(500)
|
||||
|
||||
def download_mode_clicked(self):
|
||||
self.common.log("Tab", "download_mode_clicked")
|
||||
self.mode = self.common.gui.MODE_DOWNLOAD
|
||||
self.new_tab.hide()
|
||||
|
||||
self.download_mode = DownloadMode(self)
|
||||
|
||||
self.layout.addWidget(self.download_mode)
|
||||
self.download_mode.show()
|
||||
|
||||
self.download_mode.init()
|
||||
self.download_mode.server_status.server_started.connect(
|
||||
self.update_server_status_indicator
|
||||
)
|
||||
self.download_mode.server_status.server_stopped.connect(
|
||||
self.update_server_status_indicator
|
||||
)
|
||||
self.download_mode.start_server_finished.connect(
|
||||
self.update_server_status_indicator
|
||||
)
|
||||
self.download_mode.stop_server_finished.connect(
|
||||
self.update_server_status_indicator
|
||||
)
|
||||
self.download_mode.stop_server_finished.connect(self.stop_server_finished)
|
||||
self.download_mode.start_server_finished.connect(self.clear_message)
|
||||
self.download_mode.server_status.button_clicked.connect(self.clear_message)
|
||||
|
||||
self.change_title.emit(self.tab_id, strings._("gui_tab_name_download"))
|
||||
|
||||
self.update_server_status_indicator()
|
||||
self.timer.start(500)
|
||||
|
||||
def update_server_status_indicator(self):
|
||||
# Set the status image
|
||||
if self.mode == self.common.gui.MODE_SHARE:
|
||||
@ -477,6 +516,16 @@ class Tab(QtWidgets.QWidget):
|
||||
self.set_server_status_indicator_started(
|
||||
strings._("gui_status_indicator_chat_started")
|
||||
)
|
||||
elif self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
# Download mode
|
||||
if self.download_mode.server_status.status == ServerStatus.STATUS_STOPPED:
|
||||
self.set_server_status_indicator_stopped(
|
||||
strings._("gui_status_indicator_download_stopped")
|
||||
)
|
||||
elif self.download_mode.server_status.status == ServerStatus.STATUS_WORKING:
|
||||
self.set_server_status_indicator_working(
|
||||
strings._("gui_status_indicator_download_working")
|
||||
)
|
||||
|
||||
def set_server_status_indicator_stopped(self, label_text):
|
||||
self.change_icon.emit(
|
||||
@ -537,8 +586,11 @@ class Tab(QtWidgets.QWidget):
|
||||
done = False
|
||||
while not done:
|
||||
try:
|
||||
r = mode.web.q.get(False)
|
||||
events.append(r)
|
||||
if mode.is_server:
|
||||
r = mode.web.q.get(False)
|
||||
events.append(r)
|
||||
else:
|
||||
done = True
|
||||
except queue.Empty:
|
||||
done = True
|
||||
|
||||
@ -632,8 +684,12 @@ class Tab(QtWidgets.QWidget):
|
||||
return self.receive_mode
|
||||
elif self.mode == self.common.gui.MODE_CHAT:
|
||||
return self.chat_mode
|
||||
else:
|
||||
elif self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
return self.download_mode
|
||||
elif self.mode == self.common.gui.MODE_WEBSITE:
|
||||
return self.website_mode
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -655,6 +711,10 @@ class Tab(QtWidgets.QWidget):
|
||||
dialog_text = strings._("gui_close_tab_warning_receive_description")
|
||||
elif self.mode == self.common.gui.MODE_CHAT:
|
||||
dialog_text = strings._("gui_close_tab_warning_chat_description")
|
||||
elif self.mode == self.common.gui.MODE_DOWNLOAD:
|
||||
dialog_text = strings._(
|
||||
"gui_close_tab_warning_download_description"
|
||||
)
|
||||
else:
|
||||
dialog_text = strings._("gui_close_tab_warning_website_description")
|
||||
|
||||
@ -673,7 +733,7 @@ class Tab(QtWidgets.QWidget):
|
||||
|
||||
def cleanup(self):
|
||||
self.common.log("Tab", "cleanup", f"tab_id={self.tab_id}")
|
||||
if self.get_mode():
|
||||
if self.get_mode() and self.get_mode().is_server:
|
||||
if self.get_mode().web_thread:
|
||||
self.get_mode().web.stop(self.get_mode().app.port)
|
||||
self.get_mode().web_thread.quit()
|
||||
|
@ -18,9 +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/>.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
from PySide6 import QtCore
|
||||
|
||||
from onionshare_cli.onion import (
|
||||
@ -127,6 +129,8 @@ class WebThread(QtCore.QThread):
|
||||
self.mode.common.log("WebThread", "__init__")
|
||||
|
||||
def run(self):
|
||||
if not self.mode.is_server:
|
||||
return
|
||||
self.mode.common.log("WebThread", "run")
|
||||
try:
|
||||
self.mode.web.start(self.mode.app.port)
|
||||
@ -137,6 +141,7 @@ class WebThread(QtCore.QThread):
|
||||
self.error.emit(message)
|
||||
return
|
||||
|
||||
|
||||
class AutoStartTimer(QtCore.QThread):
|
||||
"""
|
||||
Waits for a prescribed time before allowing a share to start
|
||||
@ -276,3 +281,214 @@ class OnionCleanupThread(QtCore.QThread):
|
||||
def run(self):
|
||||
self.common.log("OnionCleanupThread", "run")
|
||||
self.common.gui.onion.cleanup()
|
||||
|
||||
|
||||
class DownloadThread(QtCore.QThread):
|
||||
"""
|
||||
Download an Onion share in a separate thread (for Download Mode)
|
||||
"""
|
||||
|
||||
begun = QtCore.Signal()
|
||||
progress = QtCore.Signal(int) # progress percentage
|
||||
success = QtCore.Signal(
|
||||
str, str, int
|
||||
) # Emit file_path (str), file_name (str), and file_size (int)
|
||||
error = QtCore.Signal(str)
|
||||
locked = QtCore.Signal()
|
||||
|
||||
def __init__(self, mode):
|
||||
super(DownloadThread, self).__init__()
|
||||
self._mutex = QtCore.QMutex() # Mutex for locking
|
||||
self._lock = False # To track the lock status
|
||||
|
||||
self.mode = mode
|
||||
self.mode.common.log("DownloadThread", "__init__")
|
||||
|
||||
self.saved_download_mode_dir = None
|
||||
|
||||
def run(self):
|
||||
if self._mutex.tryLock():
|
||||
self._lock = True
|
||||
self.mode.common.log("DownloadThread", "run", "Lock claimed")
|
||||
self.begun.emit()
|
||||
|
||||
try:
|
||||
self.mode.common.log("DownloadThread", "run")
|
||||
if self.mode.common.gui.local_only:
|
||||
proxies = {}
|
||||
else:
|
||||
# Obtain the SocksPort from Tor and set it as the proxies for Requests
|
||||
(socks_address, socks_port) = (
|
||||
self.mode.common.gui.onion.get_tor_socks_port()
|
||||
)
|
||||
proxies = {
|
||||
"http": f"socks5h://{socks_address}:{socks_port}",
|
||||
"https": f"socks5h://{socks_address}:{socks_port}",
|
||||
}
|
||||
|
||||
# We only support the /download (zip) route for Share Mode right now
|
||||
# (no individual file downloads)
|
||||
url = f"http://{self.mode.service_id}.onion/download"
|
||||
|
||||
# Set up Client Auth if required
|
||||
if (
|
||||
self.mode.onionshare_uses_private_key_checkbox.isChecked()
|
||||
and len(self.mode.onionshare_private_key.text().strip()) == 52
|
||||
):
|
||||
self.mode.common.log(
|
||||
"DownloadThread", "run", f"Setting private key"
|
||||
)
|
||||
self.mode.common.gui.onion.add_onion_client_auth(
|
||||
self.mode.service_id,
|
||||
self.mode.onionshare_private_key.text().strip(),
|
||||
)
|
||||
|
||||
response = requests.get(url, proxies=proxies, stream=True)
|
||||
|
||||
# Check if the request was successful (HTTP status code 200)
|
||||
if response.status_code == 200:
|
||||
# Extract the file name from the 'Content-Disposition' header, if present
|
||||
file_name = self.get_filename_from_content_disposition(
|
||||
response.headers
|
||||
)
|
||||
self.mode.common.log(
|
||||
"DownloadThread",
|
||||
"run",
|
||||
f"file_name is {file_name}, now we will try and write it",
|
||||
)
|
||||
|
||||
# Get the file size from the 'Content-Length' header
|
||||
file_size = int(response.headers.get("Content-Length", 0))
|
||||
|
||||
# Save the share
|
||||
file_path = self.save_share(file_name, response, file_size)
|
||||
self.mode.common.log(
|
||||
"DownloadThread",
|
||||
"run",
|
||||
f"file_path is {file_path}, we saved it",
|
||||
)
|
||||
|
||||
# Emit the file path, file name, and file size to DownloadMode's add_download_item()
|
||||
self.success.emit(file_path, file_name, file_size)
|
||||
else:
|
||||
raise Exception(
|
||||
f"Failed to download file. Status code: {response.status_code}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mode.common.log(
|
||||
"DownloadThread", "run", f"Error occurred in requests: {e}"
|
||||
)
|
||||
self.error.emit(str(e))
|
||||
return
|
||||
|
||||
finally:
|
||||
# Always release the mutex, even if an exception occurred
|
||||
self._mutex.unlock()
|
||||
self._lock = False
|
||||
else:
|
||||
self.mode.common.log(
|
||||
"DownloadThread",
|
||||
"run",
|
||||
"Lock was in place, maybe the last download was still occurring?",
|
||||
)
|
||||
self.locked.emit()
|
||||
|
||||
def get_filename_from_content_disposition(self, headers):
|
||||
content_disposition = headers.get("Content-Disposition", "")
|
||||
|
||||
# Extract filename directly from Content-Disposition
|
||||
if "filename" in content_disposition:
|
||||
# Extract the filename (it could contain the UTF-8 encoded form as well)
|
||||
filename = (
|
||||
content_disposition.split("filename=")[1].split(";")[0].strip('"')
|
||||
)
|
||||
elif "filename*" in content_disposition:
|
||||
# Extract and decode filename* if filename* is used
|
||||
filename = unquote(
|
||||
content_disposition.split("filename*=UTF-8''")[1].strip('"')
|
||||
)
|
||||
else:
|
||||
# Fallback default filename
|
||||
filename = "downloaded_file.zip"
|
||||
|
||||
return filename
|
||||
|
||||
def save_share(self, file_name, response, file_size):
|
||||
"""
|
||||
Write the downloaded share to disk. This is similar to ReceiveMode,
|
||||
in that we try to create a unique timestamp-based directory to save
|
||||
to.
|
||||
|
||||
The exception is if we are in polling mode, in which case, we want
|
||||
to continually overwrite the existing download with a new version,
|
||||
to keep it up to date.
|
||||
"""
|
||||
if self.mode.is_polling and self.mode.history.completed_count > 0:
|
||||
download_mode_dir = self.saved_download_mode_dir
|
||||
|
||||
else:
|
||||
now = datetime.now()
|
||||
date_dir = now.strftime("%Y-%m-%d")
|
||||
time_dir = now.strftime("%H%M%S%f")
|
||||
download_mode_dir = os.path.join(
|
||||
self.mode.settings.get("download", "data_dir"), date_dir, time_dir
|
||||
)
|
||||
|
||||
# Create that directory, which shouldn't exist yet
|
||||
try:
|
||||
os.makedirs(download_mode_dir, 0o700, exist_ok=False)
|
||||
except OSError:
|
||||
# If this directory already exists, maybe someone else is downloading files at
|
||||
# the same second in another tab, so use a different name in that case
|
||||
if os.path.exists(download_mode_dir):
|
||||
# Keep going until we find a directory name that's available
|
||||
i = 1
|
||||
while True:
|
||||
new_download_mode_dir = f"{download_mode_dir}-{i}"
|
||||
try:
|
||||
os.makedirs(new_download_mode_dir, 0o700, exist_ok=False)
|
||||
download_mode_dir = new_download_mode_dir
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
i += 1
|
||||
# Failsafe
|
||||
if i == 100:
|
||||
self.mode.common.log(
|
||||
"DownloadThread",
|
||||
"save_share",
|
||||
"Error finding available download directory",
|
||||
)
|
||||
raise Exception(
|
||||
"Error finding available download directory"
|
||||
)
|
||||
except PermissionError:
|
||||
raise Exception("Permission denied creating download directory")
|
||||
|
||||
# Save this successful download dir if we are in polling mode, so we can
|
||||
# use it next time.
|
||||
if self.mode.is_polling and self.mode.history.completed_count == 0:
|
||||
self.saved_download_mode_dir = download_mode_dir
|
||||
|
||||
# Build file path for saving
|
||||
file_path = os.path.join(download_mode_dir, file_name)
|
||||
self.mode.common.log(
|
||||
"DownloadThread",
|
||||
"save_share",
|
||||
f"File path: {file_path}",
|
||||
)
|
||||
|
||||
# Save the file and report the progress back to the UI
|
||||
with open(file_path, "wb") as file:
|
||||
downloaded = 0
|
||||
for chunk in response.iter_content(chunk_size=1024): # 1KB chunks
|
||||
if chunk:
|
||||
downloaded += len(chunk) # Update the downloaded bytes
|
||||
|
||||
# Send progress via signal to the main thread
|
||||
progress = int((downloaded / file_size) * 100)
|
||||
self.progress.emit(progress)
|
||||
|
||||
file.write(chunk)
|
||||
return file_path
|
||||
|
Loading…
x
Reference in New Issue
Block a user