Merge branch 'develop' into update-deps
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
__pycache__
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
|
6
BUILD.md
@ -11,9 +11,11 @@ cd onionshare
|
|||||||
|
|
||||||
Install the needed dependencies:
|
Install the needed dependencies:
|
||||||
|
|
||||||
For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-socks python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy`
|
For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3`
|
||||||
|
|
||||||
For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 rpm-build`
|
On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available.
|
||||||
|
|
||||||
|
For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks`
|
||||||
|
|
||||||
After that you can try both the CLI and the GUI version of OnionShare:
|
After that you can try both the CLI and the GUI version of OnionShare:
|
||||||
|
|
||||||
|
2
LICENSE
@ -1,7 +1,7 @@
|
|||||||
(Note: Third-party licenses can be found under install/licenses/.)
|
(Note: Third-party licenses can be found under install/licenses/.)
|
||||||
|
|
||||||
OnionShare
|
OnionShare
|
||||||
Copyright © 2018 Micah Lee <micah@micahflee.com>
|
Copyright © 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -54,7 +54,12 @@ def main():
|
|||||||
|
|
||||||
dir = args.onionshare_dir
|
dir = args.onionshare_dir
|
||||||
|
|
||||||
src = files_in(dir, 'onionshare') + files_in(dir, 'onionshare_gui')
|
src = files_in(dir, 'onionshare') + \
|
||||||
|
files_in(dir, 'onionshare_gui') + \
|
||||||
|
files_in(dir, 'onionshare_gui/share_mode') + \
|
||||||
|
files_in(dir, 'onionshare_gui/receive_mode') + \
|
||||||
|
files_in(dir, 'install/scripts') + \
|
||||||
|
files_in(dir, 'test')
|
||||||
pysrc = [p for p in src if p.endswith('.py')]
|
pysrc = [p for p in src if p.endswith('.py')]
|
||||||
|
|
||||||
lang_code = args.lang_code
|
lang_code = args.lang_code
|
||||||
@ -64,10 +69,10 @@ def main():
|
|||||||
for line in fileinput.input(pysrc, openhook=fileinput.hook_encoded('utf-8')):
|
for line in fileinput.input(pysrc, openhook=fileinput.hook_encoded('utf-8')):
|
||||||
# search `strings._('translate_key')`
|
# search `strings._('translate_key')`
|
||||||
# `strings._('translate_key', True)`
|
# `strings._('translate_key', True)`
|
||||||
m = re.search(r'strings\._\((.*?)\)', line)
|
m = re.findall(r'strings\._\((.*?)\)', line)
|
||||||
if m:
|
if m:
|
||||||
arg = m.group(1)
|
for match in m:
|
||||||
key = arg.split(',')[0].strip('''"' ''')
|
key = match.split(',')[0].strip('''"' ''')
|
||||||
translate_keys.add(key)
|
translate_keys.add(key)
|
||||||
|
|
||||||
if args.show_all_keys:
|
if args.show_all_keys:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
OnionShare
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
Copyright © 2018 Micah Lee <micah@micahflee.com>
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import os, sys, time, argparse, threading
|
import os, sys, time, argparse, threading
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
from .common import Common
|
from .common import Common, DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
|
||||||
from .web import Web
|
from .web import Web
|
||||||
from .onion import *
|
from .onion import *
|
||||||
from .onionshare import OnionShare
|
from .onionshare import OnionShare
|
||||||
@ -89,38 +89,24 @@ def main(cwd=None):
|
|||||||
# Debug mode?
|
# Debug mode?
|
||||||
common.debug = debug
|
common.debug = debug
|
||||||
|
|
||||||
# In receive mode, validate downloads dir
|
|
||||||
if receive:
|
|
||||||
valid = True
|
|
||||||
if not os.path.isdir(common.settings.get('downloads_dir')):
|
|
||||||
try:
|
|
||||||
os.mkdir(common.settings.get('downloads_dir'), 0o700)
|
|
||||||
except:
|
|
||||||
print(strings._('error_cannot_create_downloads_dir').format(common.settings.get('downloads_dir')))
|
|
||||||
valid = False
|
|
||||||
if valid and not os.access(common.settings.get('downloads_dir'), os.W_OK):
|
|
||||||
print(strings._('error_downloads_dir_not_writable').format(common.settings.get('downloads_dir')))
|
|
||||||
valid = False
|
|
||||||
if not valid:
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Create the Web object
|
# Create the Web object
|
||||||
web = Web(common, stay_open, False, receive)
|
web = Web(common, False, receive)
|
||||||
|
|
||||||
# Start the Onion object
|
# Start the Onion object
|
||||||
onion = Onion(common)
|
onion = Onion(common)
|
||||||
try:
|
try:
|
||||||
onion.connect(custom_settings=False, config=config)
|
onion.connect(custom_settings=False, config=config)
|
||||||
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e:
|
|
||||||
sys.exit(e.args[0])
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("")
|
print("")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
except Exception as e:
|
||||||
|
sys.exit(e.args[0])
|
||||||
|
|
||||||
# Start the onionshare app
|
# Start the onionshare app
|
||||||
try:
|
try:
|
||||||
app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout)
|
app = OnionShare(common, onion, local_only, shutdown_timeout)
|
||||||
app.set_stealth(stealth)
|
app.set_stealth(stealth)
|
||||||
|
app.choose_port()
|
||||||
app.start_onion_service()
|
app.start_onion_service()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("")
|
print("")
|
||||||
@ -142,7 +128,7 @@ def main(cwd=None):
|
|||||||
print('')
|
print('')
|
||||||
|
|
||||||
# Start OnionShare http service in new thread
|
# Start OnionShare http service in new thread
|
||||||
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, common.settings.get('slug')))
|
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug')))
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
@ -160,6 +146,12 @@ def main(cwd=None):
|
|||||||
common.settings.set('slug', web.slug)
|
common.settings.set('slug', web.slug)
|
||||||
common.settings.save()
|
common.settings.save()
|
||||||
|
|
||||||
|
# Build the URL
|
||||||
|
if common.settings.get('public_mode'):
|
||||||
|
url = 'http://{0:s}'.format(app.onion_host)
|
||||||
|
else:
|
||||||
|
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
|
||||||
|
|
||||||
print('')
|
print('')
|
||||||
if receive:
|
if receive:
|
||||||
print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir')))
|
print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir')))
|
||||||
@ -169,19 +161,19 @@ def main(cwd=None):
|
|||||||
|
|
||||||
if stealth:
|
if stealth:
|
||||||
print(strings._("give_this_url_receive_stealth"))
|
print(strings._("give_this_url_receive_stealth"))
|
||||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
print(url)
|
||||||
print(app.auth_string)
|
print(app.auth_string)
|
||||||
else:
|
else:
|
||||||
print(strings._("give_this_url_receive"))
|
print(strings._("give_this_url_receive"))
|
||||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
print(url)
|
||||||
else:
|
else:
|
||||||
if stealth:
|
if stealth:
|
||||||
print(strings._("give_this_url_stealth"))
|
print(strings._("give_this_url_stealth"))
|
||||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
print(url)
|
||||||
print(app.auth_string)
|
print(app.auth_string)
|
||||||
else:
|
else:
|
||||||
print(strings._("give_this_url"))
|
print(strings._("give_this_url"))
|
||||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
print(url)
|
||||||
print('')
|
print('')
|
||||||
print(strings._("ctrlc_to_stop"))
|
print(strings._("ctrlc_to_stop"))
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -31,6 +31,21 @@ import time
|
|||||||
|
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadsDirErrorCannotCreate(Exception):
|
||||||
|
"""
|
||||||
|
Error creating the downloads dir (~/OnionShare by default).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadsDirErrorNotWritable(Exception):
|
||||||
|
"""
|
||||||
|
Downloads dir is not writable.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Common(object):
|
class Common(object):
|
||||||
"""
|
"""
|
||||||
The Common object is shared amongst all parts of OnionShare.
|
The Common object is shared amongst all parts of OnionShare.
|
||||||
@ -132,6 +147,226 @@ class Common(object):
|
|||||||
r = random.SystemRandom()
|
r = random.SystemRandom()
|
||||||
return '-'.join(r.choice(wordlist) for _ in range(2))
|
return '-'.join(r.choice(wordlist) for _ in range(2))
|
||||||
|
|
||||||
|
def define_css(self):
|
||||||
|
"""
|
||||||
|
This defines all of the stylesheets used in GUI mode, to avoid repeating code.
|
||||||
|
This method is only called in GUI mode.
|
||||||
|
"""
|
||||||
|
self.css = {
|
||||||
|
# OnionShareGui styles
|
||||||
|
'mode_switcher_selected_style': """
|
||||||
|
QPushButton {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #4e064f;
|
||||||
|
border: 0;
|
||||||
|
border-right: 1px solid #69266b;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 0;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'mode_switcher_unselected_style': """
|
||||||
|
QPushButton {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #601f61;
|
||||||
|
border: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
border-radius: 0;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'settings_button': """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #601f61;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid #69266b;
|
||||||
|
border-radius: 0;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'server_status_indicator_label': """
|
||||||
|
QLabel {
|
||||||
|
font-style: italic;
|
||||||
|
color: #666666;
|
||||||
|
padding: 2px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'status_bar': """
|
||||||
|
QStatusBar {
|
||||||
|
font-style: italic;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
QStatusBar::item {
|
||||||
|
border: 0px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
# Common styles between ShareMode and ReceiveMode and their child widgets
|
||||||
|
'mode_info_label': """
|
||||||
|
QLabel {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'server_status_url': """
|
||||||
|
QLabel {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #666666;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'server_status_url_buttons': """
|
||||||
|
QPushButton {
|
||||||
|
color: #3f7fcf;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'server_status_button_stopped': """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #5fa416;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'server_status_button_working': """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #4c8211;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'server_status_button_started': """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #d0011b;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'downloads_uploads_label': """
|
||||||
|
QLabel {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size 14px;
|
||||||
|
text-align: center;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'downloads_uploads_progress_bar': """
|
||||||
|
QProgressBar {
|
||||||
|
border: 1px solid #4e064f;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
text-align: center;
|
||||||
|
color: #9b9b9b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #4e064f;
|
||||||
|
width: 10px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
# Share mode and child widget styles
|
||||||
|
'share_zip_progess_bar': """
|
||||||
|
QProgressBar {
|
||||||
|
border: 1px solid #4e064f;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
text-align: center;
|
||||||
|
color: #9b9b9b;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
border: 0px;
|
||||||
|
background-color: #4e064f;
|
||||||
|
width: 10px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'share_filesize_warning': """
|
||||||
|
QLabel {
|
||||||
|
padding: 10px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'share_file_selection_drop_here_label': """
|
||||||
|
QLabel {
|
||||||
|
color: #999999;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'share_file_selection_drop_count_label': """
|
||||||
|
QLabel {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #f44449;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'share_file_list_drag_enter': """
|
||||||
|
FileList {
|
||||||
|
border: 3px solid #538ad0;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'share_file_list_drag_leave': """
|
||||||
|
FileList {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'share_file_list_item_size': """
|
||||||
|
QLabel {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 11px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
# Receive mode and child widget styles
|
||||||
|
'receive_file': """
|
||||||
|
QWidget {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
|
||||||
|
'receive_file_size': """
|
||||||
|
QLabel {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 11px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
# Settings dialog
|
||||||
|
'settings_version': """
|
||||||
|
QLabel {
|
||||||
|
color: #666666;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'settings_tor_status': """
|
||||||
|
QLabel {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
padding: 10px;
|
||||||
|
}""",
|
||||||
|
|
||||||
|
'settings_whats_this': """
|
||||||
|
QLabel {
|
||||||
|
font-size: 12px;
|
||||||
|
}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_downloads_dir(self):
|
||||||
|
"""
|
||||||
|
Validate that downloads_dir exists, and create it if it doesn't
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(self.settings.get('downloads_dir')):
|
||||||
|
try:
|
||||||
|
os.mkdir(self.settings.get('downloads_dir'), 0o700)
|
||||||
|
except:
|
||||||
|
raise DownloadsDirErrorCannotCreate
|
||||||
|
|
||||||
|
if not os.access(self.settings.get('downloads_dir'), os.W_OK):
|
||||||
|
raise DownloadsDirErrorNotWritable
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_string(num_bytes, output_len=None):
|
def random_string(num_bytes, output_len=None):
|
||||||
"""
|
"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -21,8 +21,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
from stem.control import Controller
|
from stem.control import Controller
|
||||||
from stem import ProtocolError, SocketClosed
|
from stem import ProtocolError, SocketClosed
|
||||||
from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure
|
from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure
|
||||||
import os, sys, tempfile, shutil, urllib, platform, subprocess, time, shlex
|
import base64, os, sys, tempfile, shutil, urllib, platform, subprocess, time, shlex
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion as Version
|
||||||
|
from . import onionkey
|
||||||
from . import common, strings
|
from . import common, strings
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
|
||||||
@ -437,13 +439,41 @@ class Onion(object):
|
|||||||
basic_auth = None
|
basic_auth = None
|
||||||
|
|
||||||
if self.settings.get('private_key'):
|
if self.settings.get('private_key'):
|
||||||
key_type = "RSA1024"
|
|
||||||
key_content = self.settings.get('private_key')
|
key_content = self.settings.get('private_key')
|
||||||
self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a saved private key')
|
# is the key a v2 key?
|
||||||
|
if onionkey.is_v2_key(key_content):
|
||||||
|
key_type = "RSA1024"
|
||||||
|
# The below section is commented out because re-publishing
|
||||||
|
# a pre-prepared v3 private key is currently unstable in Tor.
|
||||||
|
# This is fixed upstream but won't reach stable until 0.3.5
|
||||||
|
# (expected in December 2018)
|
||||||
|
# See https://trac.torproject.org/projects/tor/ticket/25552
|
||||||
|
# Until then, we will deliberately not work with 'persistent'
|
||||||
|
# v3 onions, which should not be possible via the GUI settings
|
||||||
|
# anyway.
|
||||||
|
# Our ticket: https://github.com/micahflee/onionshare/issues/677
|
||||||
|
#
|
||||||
|
# Assume it was a v3 key
|
||||||
|
# key_type = "ED25519-V3"
|
||||||
else:
|
else:
|
||||||
key_type = "NEW"
|
raise TorErrorProtocolError(strings._('error_invalid_private_key'))
|
||||||
key_content = "RSA1024"
|
self.common.log('Onion', 'Starting a hidden service with a saved private key')
|
||||||
self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a new private key')
|
else:
|
||||||
|
# Work out if we can support v3 onion services, which are preferred
|
||||||
|
if Version(self.tor_version) >= Version('0.3.2.9') and not self.settings.get('use_legacy_v2_onions'):
|
||||||
|
key_type = "ED25519-V3"
|
||||||
|
key_content = onionkey.generate_v3_private_key()[0]
|
||||||
|
else:
|
||||||
|
# fall back to v2 onion services
|
||||||
|
key_type = "RSA1024"
|
||||||
|
key_content = onionkey.generate_v2_private_key()[0]
|
||||||
|
self.common.log('Onion', 'Starting a hidden service with a new private key')
|
||||||
|
|
||||||
|
# v3 onions don't yet support basic auth. Our ticket:
|
||||||
|
# https://github.com/micahflee/onionshare/issues/697
|
||||||
|
if key_type == "ED25519-V3" and not self.settings.get('use_legacy_v2_onions'):
|
||||||
|
basic_auth = None
|
||||||
|
self.stealth = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if basic_auth != None:
|
if basic_auth != None:
|
||||||
@ -461,7 +491,7 @@ class Onion(object):
|
|||||||
# A new private key was generated and is in the Control port response.
|
# A new private key was generated and is in the Control port response.
|
||||||
if self.settings.get('save_private_key'):
|
if self.settings.get('save_private_key'):
|
||||||
if not self.settings.get('private_key'):
|
if not self.settings.get('private_key'):
|
||||||
self.settings.set('private_key', res.private_key)
|
self.settings.set('private_key', key_content)
|
||||||
|
|
||||||
if self.stealth:
|
if self.stealth:
|
||||||
# Similar to the PrivateKey, the Control port only returns the ClientAuth
|
# Similar to the PrivateKey, the Control port only returns the ClientAuth
|
||||||
|
129
onionshare/onionkey.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
|
Copyright (C) 2017 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 os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
# Need sha3 if python version is older than 3.6, otherwise
|
||||||
|
# we can't use hashlib.sha3_256
|
||||||
|
if sys.version_info < (3, 6):
|
||||||
|
import sha3
|
||||||
|
|
||||||
|
import nacl.signing
|
||||||
|
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
|
|
||||||
|
def stem_compatible_base64_blob_from_private_key(private_key_seed: bytes) -> str:
|
||||||
|
"""
|
||||||
|
Provides a base64-encoded private key for v3-style Onions.
|
||||||
|
"""
|
||||||
|
b = 256
|
||||||
|
|
||||||
|
def bit(h: bytes, i: int) -> int:
|
||||||
|
return (h[i // 8] >> (i % 8)) & 1
|
||||||
|
|
||||||
|
def encode_int(y: int) -> bytes:
|
||||||
|
bits = [(y >> i) & 1 for i in range(b)]
|
||||||
|
return b''.join([bytes([(sum([bits[i * 8 + j] << j for j in range(8)]))]) for i in range(b // 8)])
|
||||||
|
|
||||||
|
def expand_private_key(sk: bytes) -> bytes:
|
||||||
|
h = hashlib.sha512(sk).digest()
|
||||||
|
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
|
||||||
|
k = b''.join([bytes([h[i]]) for i in range(b // 8, b // 4)])
|
||||||
|
assert len(k) == 32
|
||||||
|
return encode_int(a) + k
|
||||||
|
|
||||||
|
expanded_private_key = expand_private_key(private_key_seed)
|
||||||
|
return base64.b64encode(expanded_private_key).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def onion_url_from_private_key(private_key_seed: bytes) -> str:
|
||||||
|
"""
|
||||||
|
Derives the public key (.onion hostname) from a v3-style
|
||||||
|
Onion private key.
|
||||||
|
"""
|
||||||
|
signing_key = nacl.signing.SigningKey(seed=private_key_seed)
|
||||||
|
public_key = bytes(signing_key.verify_key)
|
||||||
|
version = b'\x03'
|
||||||
|
checksum = hashlib.sha3_256(b".onion checksum" + public_key + version).digest()[:2]
|
||||||
|
onion_address = "http://{}.onion".format(base64.b32encode(public_key + checksum + version).decode().lower())
|
||||||
|
return onion_address
|
||||||
|
|
||||||
|
|
||||||
|
def generate_v3_private_key():
|
||||||
|
"""
|
||||||
|
Generates a private and public key for use with v3 style Onions.
|
||||||
|
Returns both the private key as well as the public key (.onion hostname)
|
||||||
|
"""
|
||||||
|
private_key_seed = os.urandom(32)
|
||||||
|
private_key = stem_compatible_base64_blob_from_private_key(private_key_seed)
|
||||||
|
onion_url = onion_url_from_private_key(private_key_seed)
|
||||||
|
return (private_key, onion_url)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_v2_private_key():
|
||||||
|
"""
|
||||||
|
Generates a private and public key for use with v2 style Onions.
|
||||||
|
Returns both the serialized private key (compatible with Stem)
|
||||||
|
as well as the public key (.onion hostname)
|
||||||
|
"""
|
||||||
|
# Generate v2 Onion Service private key
|
||||||
|
private_key = rsa.generate_private_key(public_exponent=65537,
|
||||||
|
key_size=1024,
|
||||||
|
backend=default_backend())
|
||||||
|
hs_public_key = private_key.public_key()
|
||||||
|
|
||||||
|
# Pre-generate the public key (.onion hostname)
|
||||||
|
der_format = hs_public_key.public_bytes(encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PublicFormat.PKCS1)
|
||||||
|
|
||||||
|
onion_url = base64.b32encode(hashlib.sha1(der_format).digest()[:-10]).lower().decode()
|
||||||
|
|
||||||
|
# Generate Stem-compatible key content
|
||||||
|
pem_format = private_key.private_bytes(encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption())
|
||||||
|
serialized_key = ''.join(pem_format.decode().split('\n')[1:-2])
|
||||||
|
|
||||||
|
return (serialized_key, onion_url)
|
||||||
|
|
||||||
|
|
||||||
|
def is_v2_key(key):
|
||||||
|
"""
|
||||||
|
Helper function for determining if a key is RSA1024 (v2) or not.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Import the key
|
||||||
|
key = RSA.importKey(base64.b64decode(key))
|
||||||
|
# Is this a v2 Onion key? (1024 bits) If so, we should keep using it.
|
||||||
|
if key.n.bit_length() == 1024:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -28,7 +28,7 @@ class OnionShare(object):
|
|||||||
OnionShare is the main application class. Pass in options and run
|
OnionShare is the main application class. Pass in options and run
|
||||||
start_onion_service and it will do the magic.
|
start_onion_service and it will do the magic.
|
||||||
"""
|
"""
|
||||||
def __init__(self, common, onion, local_only=False, stay_open=False, shutdown_timeout=0):
|
def __init__(self, common, onion, local_only=False, shutdown_timeout=0):
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
self.common.log('OnionShare', '__init__')
|
self.common.log('OnionShare', '__init__')
|
||||||
@ -38,6 +38,7 @@ class OnionShare(object):
|
|||||||
|
|
||||||
self.hidserv_dir = None
|
self.hidserv_dir = None
|
||||||
self.onion_host = None
|
self.onion_host = None
|
||||||
|
self.port = None
|
||||||
self.stealth = None
|
self.stealth = None
|
||||||
|
|
||||||
# files and dirs to delete on shutdown
|
# files and dirs to delete on shutdown
|
||||||
@ -46,9 +47,6 @@ class OnionShare(object):
|
|||||||
# do not use tor -- for development
|
# do not use tor -- for development
|
||||||
self.local_only = local_only
|
self.local_only = local_only
|
||||||
|
|
||||||
# automatically close when download is finished
|
|
||||||
self.stay_open = stay_open
|
|
||||||
|
|
||||||
# optionally shut down after N hours
|
# optionally shut down after N hours
|
||||||
self.shutdown_timeout = shutdown_timeout
|
self.shutdown_timeout = shutdown_timeout
|
||||||
# init timing thread
|
# init timing thread
|
||||||
@ -60,25 +58,31 @@ class OnionShare(object):
|
|||||||
self.stealth = stealth
|
self.stealth = stealth
|
||||||
self.onion.stealth = stealth
|
self.onion.stealth = stealth
|
||||||
|
|
||||||
|
def choose_port(self):
|
||||||
|
"""
|
||||||
|
Choose a random port.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.port = self.common.get_available_port(17600, 17650)
|
||||||
|
except:
|
||||||
|
raise OSError(strings._('no_available_port'))
|
||||||
|
|
||||||
def start_onion_service(self):
|
def start_onion_service(self):
|
||||||
"""
|
"""
|
||||||
Start the onionshare onion service.
|
Start the onionshare onion service.
|
||||||
"""
|
"""
|
||||||
self.common.log('OnionShare', 'start_onion_service')
|
self.common.log('OnionShare', 'start_onion_service')
|
||||||
|
|
||||||
# Choose a random port
|
if not self.port:
|
||||||
try:
|
self.choose_port()
|
||||||
self.port = self.common.get_available_port(17600, 17650)
|
|
||||||
except:
|
if self.shutdown_timeout > 0:
|
||||||
raise OSError(strings._('no_available_port'))
|
self.shutdown_timer = ShutdownTimer(self.common, self.shutdown_timeout)
|
||||||
|
|
||||||
if self.local_only:
|
if self.local_only:
|
||||||
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.shutdown_timeout > 0:
|
|
||||||
self.shutdown_timer = ShutdownTimer(self.common, self.shutdown_timeout)
|
|
||||||
|
|
||||||
self.onion_host = self.onion.start_onion_service(self.port)
|
self.onion_host = self.onion.start_onion_service(self.port)
|
||||||
|
|
||||||
if self.stealth:
|
if self.stealth:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -59,7 +59,6 @@ class Settings(object):
|
|||||||
'auth_type': 'no_auth',
|
'auth_type': 'no_auth',
|
||||||
'auth_password': '',
|
'auth_password': '',
|
||||||
'close_after_first_download': True,
|
'close_after_first_download': True,
|
||||||
'systray_notifications': True,
|
|
||||||
'shutdown_timeout': False,
|
'shutdown_timeout': False,
|
||||||
'use_stealth': False,
|
'use_stealth': False,
|
||||||
'use_autoupdate': True,
|
'use_autoupdate': True,
|
||||||
@ -68,11 +67,14 @@ class Settings(object):
|
|||||||
'tor_bridges_use_obfs4': False,
|
'tor_bridges_use_obfs4': False,
|
||||||
'tor_bridges_use_meek_lite_azure': False,
|
'tor_bridges_use_meek_lite_azure': False,
|
||||||
'tor_bridges_use_custom_bridges': '',
|
'tor_bridges_use_custom_bridges': '',
|
||||||
|
'use_legacy_v2_onions': False,
|
||||||
'save_private_key': False,
|
'save_private_key': False,
|
||||||
'private_key': '',
|
'private_key': '',
|
||||||
|
'public_mode': False,
|
||||||
'slug': '',
|
'slug': '',
|
||||||
'hidservauth_string': '',
|
'hidservauth_string': '',
|
||||||
'downloads_dir': self.build_default_downloads_dir()
|
'downloads_dir': self.build_default_downloads_dir(),
|
||||||
|
'receive_allow_receiver_shutdown': True
|
||||||
}
|
}
|
||||||
self._settings = {}
|
self._settings = {}
|
||||||
self.fill_in_defaults()
|
self.fill_in_defaults()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -31,6 +31,7 @@ import re
|
|||||||
import io
|
import io
|
||||||
from distutils.version import LooseVersion as Version
|
from distutils.version import LooseVersion as Version
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import (
|
from flask import (
|
||||||
@ -39,7 +40,8 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from . import strings, common
|
from . import strings
|
||||||
|
from .common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
|
||||||
|
|
||||||
|
|
||||||
# Stub out flask's show_server_banner function, to avoiding showing warnings that
|
# Stub out flask's show_server_banner function, to avoiding showing warnings that
|
||||||
@ -54,22 +56,31 @@ class Web(object):
|
|||||||
"""
|
"""
|
||||||
The Web object is the OnionShare web server, powered by flask
|
The Web object is the OnionShare web server, powered by flask
|
||||||
"""
|
"""
|
||||||
def __init__(self, common, stay_open, gui_mode, receive_mode=False):
|
REQUEST_LOAD = 0
|
||||||
|
REQUEST_STARTED = 1
|
||||||
|
REQUEST_PROGRESS = 2
|
||||||
|
REQUEST_OTHER = 3
|
||||||
|
REQUEST_CANCELED = 4
|
||||||
|
REQUEST_RATE_LIMIT = 5
|
||||||
|
REQUEST_CLOSE_SERVER = 6
|
||||||
|
REQUEST_UPLOAD_FILE_RENAMED = 7
|
||||||
|
REQUEST_UPLOAD_FINISHED = 8
|
||||||
|
REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9
|
||||||
|
REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10
|
||||||
|
|
||||||
|
def __init__(self, common, gui_mode, receive_mode=False):
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
# The flask app
|
# The flask app
|
||||||
self.app = Flask(__name__,
|
self.app = Flask(__name__,
|
||||||
static_folder=common.get_resource_path('static'),
|
static_folder=self.common.get_resource_path('static'),
|
||||||
template_folder=common.get_resource_path('templates'))
|
template_folder=self.common.get_resource_path('templates'))
|
||||||
self.app.secret_key = self.common.random_string(8)
|
self.app.secret_key = self.common.random_string(8)
|
||||||
|
|
||||||
# Debug mode?
|
# Debug mode?
|
||||||
if self.common.debug:
|
if self.common.debug:
|
||||||
self.debug_mode()
|
self.debug_mode()
|
||||||
|
|
||||||
# Stay open after the first download?
|
|
||||||
self.stay_open = stay_open
|
|
||||||
|
|
||||||
# Are we running in GUI mode?
|
# Are we running in GUI mode?
|
||||||
self.gui_mode = gui_mode
|
self.gui_mode = gui_mode
|
||||||
|
|
||||||
@ -103,17 +114,13 @@ class Web(object):
|
|||||||
('Server', 'OnionShare')
|
('Server', 'OnionShare')
|
||||||
]
|
]
|
||||||
|
|
||||||
self.REQUEST_LOAD = 0
|
|
||||||
self.REQUEST_DOWNLOAD = 1
|
|
||||||
self.REQUEST_PROGRESS = 2
|
|
||||||
self.REQUEST_OTHER = 3
|
|
||||||
self.REQUEST_CANCELED = 4
|
|
||||||
self.REQUEST_RATE_LIMIT = 5
|
|
||||||
self.q = queue.Queue()
|
self.q = queue.Queue()
|
||||||
|
|
||||||
self.slug = None
|
self.slug = None
|
||||||
|
|
||||||
self.download_count = 0
|
self.download_count = 0
|
||||||
|
self.upload_count = 0
|
||||||
|
|
||||||
self.error404_count = 0
|
self.error404_count = 0
|
||||||
|
|
||||||
# If "Stop After First Download" is checked (stay_open == False), only allow
|
# If "Stop After First Download" is checked (stay_open == False), only allow
|
||||||
@ -130,6 +137,9 @@ class Web(object):
|
|||||||
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
|
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
|
||||||
self.shutdown_slug = self.common.random_string(16)
|
self.shutdown_slug = self.common.random_string(16)
|
||||||
|
|
||||||
|
# Keep track if the server is running
|
||||||
|
self.running = False
|
||||||
|
|
||||||
# Define the ewb app routes
|
# Define the ewb app routes
|
||||||
self.common_routes()
|
self.common_routes()
|
||||||
if self.receive_mode:
|
if self.receive_mode:
|
||||||
@ -143,12 +153,20 @@ class Web(object):
|
|||||||
"""
|
"""
|
||||||
@self.app.route("/<slug_candidate>")
|
@self.app.route("/<slug_candidate>")
|
||||||
def index(slug_candidate):
|
def index(slug_candidate):
|
||||||
|
self.check_slug_candidate(slug_candidate)
|
||||||
|
return index_logic()
|
||||||
|
|
||||||
|
@self.app.route("/")
|
||||||
|
def index_public():
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
return self.error404()
|
||||||
|
return index_logic()
|
||||||
|
|
||||||
|
def index_logic(slug_candidate=''):
|
||||||
"""
|
"""
|
||||||
Render the template for the onionshare landing page.
|
Render the template for the onionshare landing page.
|
||||||
"""
|
"""
|
||||||
self.check_slug_candidate(slug_candidate)
|
self.add_request(Web.REQUEST_LOAD, request.path)
|
||||||
|
|
||||||
self.add_request(self.REQUEST_LOAD, request.path)
|
|
||||||
|
|
||||||
# Deny new downloads if "Stop After First Download" is checked and there is
|
# Deny new downloads if "Stop After First Download" is checked and there is
|
||||||
# currently a download
|
# currently a download
|
||||||
@ -158,6 +176,7 @@ class Web(object):
|
|||||||
return self.add_security_headers(r)
|
return self.add_security_headers(r)
|
||||||
|
|
||||||
# If download is allowed to continue, serve download page
|
# If download is allowed to continue, serve download page
|
||||||
|
if self.slug:
|
||||||
r = make_response(render_template(
|
r = make_response(render_template(
|
||||||
'send.html',
|
'send.html',
|
||||||
slug=self.slug,
|
slug=self.slug,
|
||||||
@ -165,15 +184,31 @@ class Web(object):
|
|||||||
filename=os.path.basename(self.zip_filename),
|
filename=os.path.basename(self.zip_filename),
|
||||||
filesize=self.zip_filesize,
|
filesize=self.zip_filesize,
|
||||||
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
|
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
|
||||||
|
else:
|
||||||
|
# If download is allowed to continue, serve download page
|
||||||
|
r = make_response(render_template(
|
||||||
|
'send.html',
|
||||||
|
file_info=self.file_info,
|
||||||
|
filename=os.path.basename(self.zip_filename),
|
||||||
|
filesize=self.zip_filesize,
|
||||||
|
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
|
||||||
return self.add_security_headers(r)
|
return self.add_security_headers(r)
|
||||||
|
|
||||||
@self.app.route("/<slug_candidate>/download")
|
@self.app.route("/<slug_candidate>/download")
|
||||||
def download(slug_candidate):
|
def download(slug_candidate):
|
||||||
|
self.check_slug_candidate(slug_candidate)
|
||||||
|
return download_logic()
|
||||||
|
|
||||||
|
@self.app.route("/download")
|
||||||
|
def download_public():
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
return self.error404()
|
||||||
|
return download_logic()
|
||||||
|
|
||||||
|
def download_logic(slug_candidate=''):
|
||||||
"""
|
"""
|
||||||
Download the zip file.
|
Download the zip file.
|
||||||
"""
|
"""
|
||||||
self.check_slug_candidate(slug_candidate)
|
|
||||||
|
|
||||||
# Deny new downloads if "Stop After First Download" is checked and there is
|
# Deny new downloads if "Stop After First Download" is checked and there is
|
||||||
# currently a download
|
# currently a download
|
||||||
deny_download = not self.stay_open and self.download_in_progress
|
deny_download = not self.stay_open and self.download_in_progress
|
||||||
@ -181,17 +216,19 @@ class Web(object):
|
|||||||
r = make_response(render_template('denied.html'))
|
r = make_response(render_template('denied.html'))
|
||||||
return self.add_security_headers(r)
|
return self.add_security_headers(r)
|
||||||
|
|
||||||
# each download has a unique id
|
# Each download has a unique id
|
||||||
download_id = self.download_count
|
download_id = self.download_count
|
||||||
self.download_count += 1
|
self.download_count += 1
|
||||||
|
|
||||||
# prepare some variables to use inside generate() function below
|
# Prepare some variables to use inside generate() function below
|
||||||
# which is outside of the request context
|
# which is outside of the request context
|
||||||
shutdown_func = request.environ.get('werkzeug.server.shutdown')
|
shutdown_func = request.environ.get('werkzeug.server.shutdown')
|
||||||
path = request.path
|
path = request.path
|
||||||
|
|
||||||
# tell GUI the download started
|
# Tell GUI the download started
|
||||||
self.add_request(self.REQUEST_DOWNLOAD, path, {'id': download_id})
|
self.add_request(Web.REQUEST_STARTED, path, {
|
||||||
|
'id': download_id}
|
||||||
|
)
|
||||||
|
|
||||||
dirname = os.path.dirname(self.zip_filename)
|
dirname = os.path.dirname(self.zip_filename)
|
||||||
basename = os.path.basename(self.zip_filename)
|
basename = os.path.basename(self.zip_filename)
|
||||||
@ -212,7 +249,9 @@ class Web(object):
|
|||||||
while not self.done:
|
while not self.done:
|
||||||
# The user has canceled the download, so stop serving the file
|
# The user has canceled the download, so stop serving the file
|
||||||
if self.client_cancel:
|
if self.client_cancel:
|
||||||
self.add_request(self.REQUEST_CANCELED, path, {'id': download_id})
|
self.add_request(Web.REQUEST_CANCELED, path, {
|
||||||
|
'id': download_id
|
||||||
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
chunk = fp.read(chunk_size)
|
chunk = fp.read(chunk_size)
|
||||||
@ -232,7 +271,10 @@ class Web(object):
|
|||||||
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
|
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
self.add_request(self.REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
|
self.add_request(Web.REQUEST_PROGRESS, path, {
|
||||||
|
'id': download_id,
|
||||||
|
'bytes': downloaded_bytes
|
||||||
|
})
|
||||||
self.done = False
|
self.done = False
|
||||||
except:
|
except:
|
||||||
# looks like the download was canceled
|
# looks like the download was canceled
|
||||||
@ -240,7 +282,9 @@ class Web(object):
|
|||||||
canceled = True
|
canceled = True
|
||||||
|
|
||||||
# tell the GUI the download has canceled
|
# tell the GUI the download has canceled
|
||||||
self.add_request(self.REQUEST_CANCELED, path, {'id': download_id})
|
self.add_request(Web.REQUEST_CANCELED, path, {
|
||||||
|
'id': download_id
|
||||||
|
})
|
||||||
|
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
@ -254,9 +298,13 @@ class Web(object):
|
|||||||
# Close the server, if necessary
|
# Close the server, if necessary
|
||||||
if not self.stay_open and not canceled:
|
if not self.stay_open and not canceled:
|
||||||
print(strings._("closing_automatically"))
|
print(strings._("closing_automatically"))
|
||||||
|
self.running = False
|
||||||
|
try:
|
||||||
if shutdown_func is None:
|
if shutdown_func is None:
|
||||||
raise RuntimeError('Not running with the Werkzeug Server')
|
raise RuntimeError('Not running with the Werkzeug Server')
|
||||||
shutdown_func()
|
shutdown_func()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
r = Response(generate())
|
r = Response(generate())
|
||||||
r.headers.set('Content-Length', self.zip_filesize)
|
r.headers.set('Content-Length', self.zip_filesize)
|
||||||
@ -270,23 +318,63 @@ class Web(object):
|
|||||||
|
|
||||||
def receive_routes(self):
|
def receive_routes(self):
|
||||||
"""
|
"""
|
||||||
The web app routes for sharing files
|
The web app routes for receiving files
|
||||||
"""
|
"""
|
||||||
@self.app.route("/<slug_candidate>")
|
def index_logic():
|
||||||
def index(slug_candidate):
|
self.add_request(Web.REQUEST_LOAD, request.path)
|
||||||
self.check_slug_candidate(slug_candidate)
|
|
||||||
|
if self.common.settings.get('public_mode'):
|
||||||
|
upload_action = '/upload'
|
||||||
|
close_action = '/close'
|
||||||
|
else:
|
||||||
|
upload_action = '/{}/upload'.format(self.slug)
|
||||||
|
close_action = '/{}/close'.format(self.slug)
|
||||||
|
|
||||||
r = make_response(render_template(
|
r = make_response(render_template(
|
||||||
'receive.html',
|
'receive.html',
|
||||||
slug=self.slug))
|
upload_action=upload_action,
|
||||||
|
close_action=close_action,
|
||||||
|
receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown')))
|
||||||
return self.add_security_headers(r)
|
return self.add_security_headers(r)
|
||||||
|
|
||||||
@self.app.route("/<slug_candidate>/upload", methods=['POST'])
|
@self.app.route("/<slug_candidate>")
|
||||||
def upload(slug_candidate):
|
def index(slug_candidate):
|
||||||
self.check_slug_candidate(slug_candidate)
|
self.check_slug_candidate(slug_candidate)
|
||||||
|
return index_logic()
|
||||||
|
|
||||||
|
@self.app.route("/")
|
||||||
|
def index_public():
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
return self.error404()
|
||||||
|
return index_logic()
|
||||||
|
|
||||||
|
|
||||||
|
def upload_logic(slug_candidate=''):
|
||||||
|
"""
|
||||||
|
Upload files.
|
||||||
|
"""
|
||||||
|
# Make sure downloads_dir exists
|
||||||
|
valid = True
|
||||||
|
try:
|
||||||
|
self.common.validate_downloads_dir()
|
||||||
|
except DownloadsDirErrorCannotCreate:
|
||||||
|
self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path)
|
||||||
|
print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
|
||||||
|
valid = False
|
||||||
|
except DownloadsDirErrorNotWritable:
|
||||||
|
self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path)
|
||||||
|
print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
|
||||||
|
valid = False
|
||||||
|
if not valid:
|
||||||
|
flash('Error uploading, please inform the OnionShare user', 'error')
|
||||||
|
if self.common.settings.get('public_mode'):
|
||||||
|
return redirect('/')
|
||||||
|
else:
|
||||||
|
return redirect('/{}'.format(slug_candidate))
|
||||||
|
|
||||||
files = request.files.getlist('file[]')
|
files = request.files.getlist('file[]')
|
||||||
filenames = []
|
filenames = []
|
||||||
|
print('')
|
||||||
for f in files:
|
for f in files:
|
||||||
if f.filename != '':
|
if f.filename != '':
|
||||||
# Automatically rename the file, if a file of the same name already exists
|
# Automatically rename the file, if a file of the same name already exists
|
||||||
@ -321,6 +409,15 @@ class Web(object):
|
|||||||
else:
|
else:
|
||||||
valid = True
|
valid = True
|
||||||
|
|
||||||
|
basename = os.path.basename(local_path)
|
||||||
|
if f.filename != basename:
|
||||||
|
# Tell the GUI that the file has changed names
|
||||||
|
self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, {
|
||||||
|
'id': request.upload_id,
|
||||||
|
'old_filename': f.filename,
|
||||||
|
'new_filename': basename
|
||||||
|
})
|
||||||
|
|
||||||
self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
|
self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
|
||||||
print(strings._('receive_mode_received_file').format(local_path))
|
print(strings._('receive_mode_received_file').format(local_path))
|
||||||
f.save(local_path)
|
f.save(local_path)
|
||||||
@ -328,19 +425,47 @@ class Web(object):
|
|||||||
# Note that flash strings are on English, and not translated, on purpose,
|
# Note that flash strings are on English, and not translated, on purpose,
|
||||||
# to avoid leaking the locale of the OnionShare user
|
# to avoid leaking the locale of the OnionShare user
|
||||||
if len(filenames) == 0:
|
if len(filenames) == 0:
|
||||||
flash('No files uploaded')
|
flash('No files uploaded', 'info')
|
||||||
else:
|
else:
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
flash('Uploaded {}'.format(filename))
|
flash('Sent {}'.format(filename), 'info')
|
||||||
|
|
||||||
|
if self.common.settings.get('public_mode'):
|
||||||
|
return redirect('/')
|
||||||
|
else:
|
||||||
|
return redirect('/{}'.format(slug_candidate))
|
||||||
|
|
||||||
|
@self.app.route("/<slug_candidate>/upload", methods=['POST'])
|
||||||
|
def upload(slug_candidate):
|
||||||
|
self.check_slug_candidate(slug_candidate)
|
||||||
|
return upload_logic(slug_candidate)
|
||||||
|
|
||||||
|
@self.app.route("/upload", methods=['POST'])
|
||||||
|
def upload_public():
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
return self.error404()
|
||||||
|
return upload_logic()
|
||||||
|
|
||||||
|
|
||||||
|
def close_logic(slug_candidate=''):
|
||||||
|
if self.common.settings.get('receive_allow_receiver_shutdown'):
|
||||||
|
self.force_shutdown()
|
||||||
|
r = make_response(render_template('closed.html'))
|
||||||
|
self.add_request(Web.REQUEST_CLOSE_SERVER, request.path)
|
||||||
|
return self.add_security_headers(r)
|
||||||
|
else:
|
||||||
return redirect('/{}'.format(slug_candidate))
|
return redirect('/{}'.format(slug_candidate))
|
||||||
|
|
||||||
@self.app.route("/<slug_candidate>/close", methods=['POST'])
|
@self.app.route("/<slug_candidate>/close", methods=['POST'])
|
||||||
def close(slug_candidate):
|
def close(slug_candidate):
|
||||||
self.check_slug_candidate(slug_candidate)
|
self.check_slug_candidate(slug_candidate)
|
||||||
self.force_shutdown()
|
return close_logic(slug_candidate)
|
||||||
r = make_response(render_template('closed.html'))
|
|
||||||
return self.add_security_headers(r)
|
@self.app.route("/close", methods=['POST'])
|
||||||
|
def close_public():
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
return self.error404()
|
||||||
|
return close_logic()
|
||||||
|
|
||||||
def common_routes(self):
|
def common_routes(self):
|
||||||
"""
|
"""
|
||||||
@ -351,27 +476,32 @@ class Web(object):
|
|||||||
"""
|
"""
|
||||||
404 error page.
|
404 error page.
|
||||||
"""
|
"""
|
||||||
self.add_request(self.REQUEST_OTHER, request.path)
|
return self.error404()
|
||||||
|
|
||||||
if request.path != '/favicon.ico':
|
|
||||||
self.error404_count += 1
|
|
||||||
if self.error404_count == 20:
|
|
||||||
self.add_request(self.REQUEST_RATE_LIMIT, request.path)
|
|
||||||
self.force_shutdown()
|
|
||||||
print(strings._('error_rate_limit'))
|
|
||||||
|
|
||||||
r = make_response(render_template('404.html'), 404)
|
|
||||||
return self.add_security_headers(r)
|
|
||||||
|
|
||||||
@self.app.route("/<slug_candidate>/shutdown")
|
@self.app.route("/<slug_candidate>/shutdown")
|
||||||
def shutdown(slug_candidate):
|
def shutdown(slug_candidate):
|
||||||
"""
|
"""
|
||||||
Stop the flask web server, from the context of an http request.
|
Stop the flask web server, from the context of an http request.
|
||||||
"""
|
"""
|
||||||
self.check_slug_candidate(slug_candidate, self.shutdown_slug)
|
self.check_shutdown_slug_candidate(slug_candidate)
|
||||||
self.force_shutdown()
|
self.force_shutdown()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def error404(self):
|
||||||
|
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||||
|
if request.path != '/favicon.ico':
|
||||||
|
self.error404_count += 1
|
||||||
|
|
||||||
|
# In receive mode, with public mode enabled, skip rate limiting 404s
|
||||||
|
if not self.common.settings.get('public_mode'):
|
||||||
|
if self.error404_count == 20:
|
||||||
|
self.add_request(Web.REQUEST_RATE_LIMIT, request.path)
|
||||||
|
self.force_shutdown()
|
||||||
|
print(strings._('error_rate_limit'))
|
||||||
|
|
||||||
|
r = make_response(render_template('404.html'), 404)
|
||||||
|
return self.add_security_headers(r)
|
||||||
|
|
||||||
def add_security_headers(self, r):
|
def add_security_headers(self, r):
|
||||||
"""
|
"""
|
||||||
Add security headers to a request
|
Add security headers to a request
|
||||||
@ -429,11 +559,14 @@ class Web(object):
|
|||||||
'data': data
|
'data': data
|
||||||
})
|
})
|
||||||
|
|
||||||
def generate_slug(self, persistent_slug=''):
|
def generate_slug(self, persistent_slug=None):
|
||||||
if persistent_slug:
|
self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug))
|
||||||
|
if persistent_slug != None and persistent_slug != '':
|
||||||
self.slug = persistent_slug
|
self.slug = persistent_slug
|
||||||
|
self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug))
|
||||||
else:
|
else:
|
||||||
self.slug = self.common.build_slug()
|
self.slug = self.common.build_slug()
|
||||||
|
self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
|
||||||
|
|
||||||
def debug_mode(self):
|
def debug_mode(self):
|
||||||
"""
|
"""
|
||||||
@ -445,26 +578,38 @@ class Web(object):
|
|||||||
log_handler.setLevel(logging.WARNING)
|
log_handler.setLevel(logging.WARNING)
|
||||||
self.app.logger.addHandler(log_handler)
|
self.app.logger.addHandler(log_handler)
|
||||||
|
|
||||||
def check_slug_candidate(self, slug_candidate, slug_compare=None):
|
def check_slug_candidate(self, slug_candidate):
|
||||||
if not slug_compare:
|
self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate))
|
||||||
slug_compare = self.slug
|
if self.common.settings.get('public_mode'):
|
||||||
if not hmac.compare_digest(slug_compare, slug_candidate):
|
abort(404)
|
||||||
|
if not hmac.compare_digest(self.slug, slug_candidate):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
def check_shutdown_slug_candidate(self, slug_candidate):
|
||||||
|
self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate))
|
||||||
|
if not hmac.compare_digest(self.shutdown_slug, slug_candidate):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def force_shutdown(self):
|
def force_shutdown(self):
|
||||||
"""
|
"""
|
||||||
Stop the flask web server, from the context of the flask app.
|
Stop the flask web server, from the context of the flask app.
|
||||||
"""
|
"""
|
||||||
# shutdown the flask service
|
# Shutdown the flask service
|
||||||
|
try:
|
||||||
func = request.environ.get('werkzeug.server.shutdown')
|
func = request.environ.get('werkzeug.server.shutdown')
|
||||||
if func is None:
|
if func is None:
|
||||||
raise RuntimeError('Not running with the Werkzeug Server')
|
raise RuntimeError('Not running with the Werkzeug Server')
|
||||||
func()
|
func()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.running = False
|
||||||
|
|
||||||
def start(self, port, stay_open=False, persistent_slug=''):
|
def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
|
||||||
"""
|
"""
|
||||||
Start the flask web server.
|
Start the flask web server.
|
||||||
"""
|
"""
|
||||||
|
self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug))
|
||||||
|
if not public_mode:
|
||||||
self.generate_slug(persistent_slug)
|
self.generate_slug(persistent_slug)
|
||||||
|
|
||||||
self.stay_open = stay_open
|
self.stay_open = stay_open
|
||||||
@ -475,6 +620,7 @@ class Web(object):
|
|||||||
else:
|
else:
|
||||||
host = '127.0.0.1'
|
host = '127.0.0.1'
|
||||||
|
|
||||||
|
self.running = True
|
||||||
self.app.run(host=host, port=port, threaded=True)
|
self.app.run(host=host, port=port, threaded=True)
|
||||||
|
|
||||||
def stop(self, port):
|
def stop(self, port):
|
||||||
@ -486,7 +632,8 @@ class Web(object):
|
|||||||
# serving the file
|
# serving the file
|
||||||
self.client_cancel = True
|
self.client_cancel = True
|
||||||
|
|
||||||
# to stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
|
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
|
||||||
|
if self.running:
|
||||||
try:
|
try:
|
||||||
s = socket.socket()
|
s = socket.socket()
|
||||||
s.connect(('127.0.0.1', port))
|
s.connect(('127.0.0.1', port))
|
||||||
@ -561,21 +708,23 @@ class ReceiveModeWSGIMiddleware(object):
|
|||||||
environ['web'] = self.web
|
environ['web'] = self.web
|
||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
|
|
||||||
|
|
||||||
class ReceiveModeTemporaryFile(object):
|
class ReceiveModeTemporaryFile(object):
|
||||||
"""
|
"""
|
||||||
A custom TemporaryFile that tells ReceiveModeRequest every time data gets
|
A custom TemporaryFile that tells ReceiveModeRequest every time data gets
|
||||||
written to it, in order to track the progress of uploads.
|
written to it, in order to track the progress of uploads.
|
||||||
"""
|
"""
|
||||||
def __init__(self, filename, update_func):
|
def __init__(self, filename, write_func, close_func):
|
||||||
self.onionshare_filename = filename
|
self.onionshare_filename = filename
|
||||||
self.onionshare_update_func = update_func
|
self.onionshare_write_func = write_func
|
||||||
|
self.onionshare_close_func = close_func
|
||||||
|
|
||||||
# Create a temporary file
|
# Create a temporary file
|
||||||
self.f = tempfile.TemporaryFile('wb+')
|
self.f = tempfile.TemporaryFile('wb+')
|
||||||
|
|
||||||
# Make all the file-like methods and attributes actually access the
|
# Make all the file-like methods and attributes actually access the
|
||||||
# TemporaryFile, except for write
|
# TemporaryFile, except for write
|
||||||
attrs = ['close', 'closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
|
attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
|
||||||
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
|
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
|
||||||
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
|
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
|
||||||
'truncate', 'writable', 'writelines']
|
'truncate', 'writable', 'writelines']
|
||||||
@ -584,10 +733,17 @@ class ReceiveModeTemporaryFile(object):
|
|||||||
|
|
||||||
def write(self, b):
|
def write(self, b):
|
||||||
"""
|
"""
|
||||||
Custom write method that calls out to onionshare_update_func
|
Custom write method that calls out to onionshare_write_func
|
||||||
"""
|
"""
|
||||||
bytes_written = self.f.write(b)
|
bytes_written = self.f.write(b)
|
||||||
self.onionshare_update_func(self.onionshare_filename, bytes_written)
|
self.onionshare_write_func(self.onionshare_filename, bytes_written)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Custom close method that calls out to onionshare_close_func
|
||||||
|
"""
|
||||||
|
self.f.close()
|
||||||
|
self.onionshare_close_func(self.onionshare_filename)
|
||||||
|
|
||||||
|
|
||||||
class ReceiveModeRequest(Request):
|
class ReceiveModeRequest(Request):
|
||||||
@ -599,30 +755,92 @@ class ReceiveModeRequest(Request):
|
|||||||
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
|
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
|
||||||
self.web = environ['web']
|
self.web = environ['web']
|
||||||
|
|
||||||
|
# Is this a valid upload request?
|
||||||
|
self.upload_request = False
|
||||||
|
if self.method == 'POST':
|
||||||
|
if self.path == '/{}/upload'.format(self.web.slug):
|
||||||
|
self.upload_request = True
|
||||||
|
else:
|
||||||
|
if self.web.common.settings.get('public_mode'):
|
||||||
|
if self.path == '/upload':
|
||||||
|
self.upload_request = True
|
||||||
|
|
||||||
|
if self.upload_request:
|
||||||
# A dictionary that maps filenames to the bytes uploaded so far
|
# A dictionary that maps filenames to the bytes uploaded so far
|
||||||
self.onionshare_progress = {}
|
self.progress = {}
|
||||||
|
|
||||||
|
# Create an upload_id, attach it to the request
|
||||||
|
self.upload_id = self.web.upload_count
|
||||||
|
self.web.upload_count += 1
|
||||||
|
|
||||||
|
# Figure out the content length
|
||||||
|
try:
|
||||||
|
self.content_length = int(self.headers['Content-Length'])
|
||||||
|
except:
|
||||||
|
self.content_length = 0
|
||||||
|
|
||||||
|
print("{}: {}".format(
|
||||||
|
datetime.now().strftime("%b %d, %I:%M%p"),
|
||||||
|
strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
|
||||||
|
))
|
||||||
|
|
||||||
|
# Tell the GUI
|
||||||
|
self.web.add_request(Web.REQUEST_STARTED, self.path, {
|
||||||
|
'id': self.upload_id,
|
||||||
|
'content_length': self.content_length
|
||||||
|
})
|
||||||
|
|
||||||
|
self.previous_file = None
|
||||||
|
|
||||||
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
|
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
|
||||||
"""
|
"""
|
||||||
This gets called for each file that gets uploaded, and returns an file-like
|
This gets called for each file that gets uploaded, and returns an file-like
|
||||||
writable stream.
|
writable stream.
|
||||||
"""
|
"""
|
||||||
if len(self.onionshare_progress) > 0:
|
if self.upload_request:
|
||||||
print('')
|
self.progress[filename] = {
|
||||||
self.onionshare_progress[filename] = 0
|
'uploaded_bytes': 0,
|
||||||
return ReceiveModeTemporaryFile(filename, self.onionshare_update_func)
|
'complete': False
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""
|
"""
|
||||||
When closing the request, print a newline if this was a file upload.
|
Closing the request.
|
||||||
"""
|
"""
|
||||||
super(ReceiveModeRequest, self).close()
|
super(ReceiveModeRequest, self).close()
|
||||||
if len(self.onionshare_progress) > 0:
|
if self.upload_request:
|
||||||
print('')
|
# Inform the GUI that the upload has finished
|
||||||
|
self.web.add_request(Web.REQUEST_UPLOAD_FINISHED, self.path, {
|
||||||
|
'id': self.upload_id
|
||||||
|
})
|
||||||
|
|
||||||
def onionshare_update_func(self, filename, length):
|
def file_write_func(self, filename, length):
|
||||||
"""
|
"""
|
||||||
Keep track of the bytes uploaded so far for all files.
|
This function gets called when a specific file is written to.
|
||||||
"""
|
"""
|
||||||
self.onionshare_progress[filename] += length
|
if self.upload_request:
|
||||||
print('{} - {} '.format(self.web.common.human_readable_filesize(self.onionshare_progress[filename]), filename), end='\r')
|
self.progress[filename]['uploaded_bytes'] += length
|
||||||
|
|
||||||
|
if self.previous_file != filename:
|
||||||
|
if self.previous_file is not None:
|
||||||
|
print('')
|
||||||
|
self.previous_file = filename
|
||||||
|
|
||||||
|
print('\r=> {:15s} {}'.format(
|
||||||
|
self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
|
||||||
|
filename
|
||||||
|
), end='')
|
||||||
|
|
||||||
|
# Update the GUI on the upload progress
|
||||||
|
self.web.add_request(Web.REQUEST_PROGRESS, self.path, {
|
||||||
|
'id': self.upload_id,
|
||||||
|
'progress': self.progress
|
||||||
|
})
|
||||||
|
|
||||||
|
def file_close_func(self, filename):
|
||||||
|
"""
|
||||||
|
This function gets called when a specific file is closed.
|
||||||
|
"""
|
||||||
|
self.progress[filename]['complete'] = True
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -19,12 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
import os, sys, platform, argparse
|
import os, sys, platform, argparse
|
||||||
from .alert import Alert
|
from .widgets import Alert
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
from onionshare import strings
|
from onionshare import strings
|
||||||
from onionshare.common import Common
|
from onionshare.common import Common
|
||||||
from onionshare.web import Web
|
|
||||||
from onionshare.onion import Onion
|
from onionshare.onion import Onion
|
||||||
from onionshare.onionshare import OnionShare
|
from onionshare.onionshare import OnionShare
|
||||||
|
|
||||||
@ -54,6 +53,7 @@ def main():
|
|||||||
The main() function implements all of the logic that the GUI version of onionshare uses.
|
The main() function implements all of the logic that the GUI version of onionshare uses.
|
||||||
"""
|
"""
|
||||||
common = Common()
|
common = Common()
|
||||||
|
common.define_css()
|
||||||
|
|
||||||
strings.load_strings(common)
|
strings.load_strings(common)
|
||||||
print(strings._('version_string').format(common.version))
|
print(strings._('version_string').format(common.version))
|
||||||
@ -65,8 +65,6 @@ def main():
|
|||||||
# Parse arguments
|
# Parse arguments
|
||||||
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48))
|
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48))
|
||||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
|
||||||
parser.add_argument('--shutdown-timeout', metavar='<int>', dest='shutdown_timeout', default=0, help=strings._("help_shutdown_timeout"))
|
|
||||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||||
parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename'))
|
parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename'))
|
||||||
parser.add_argument('--config', metavar='config', default=False, help=strings._('help_config'))
|
parser.add_argument('--config', metavar='config', default=False, help=strings._('help_config'))
|
||||||
@ -80,8 +78,6 @@ def main():
|
|||||||
config = args.config
|
config = args.config
|
||||||
|
|
||||||
local_only = bool(args.local_only)
|
local_only = bool(args.local_only)
|
||||||
stay_open = bool(args.stay_open)
|
|
||||||
shutdown_timeout = int(args.shutdown_timeout)
|
|
||||||
debug = bool(args.debug)
|
debug = bool(args.debug)
|
||||||
|
|
||||||
# Debug mode?
|
# Debug mode?
|
||||||
@ -100,17 +96,14 @@ def main():
|
|||||||
if not valid:
|
if not valid:
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
# Create the Web object
|
|
||||||
web = Web(common, stay_open, True)
|
|
||||||
|
|
||||||
# Start the Onion
|
# Start the Onion
|
||||||
onion = Onion(common)
|
onion = Onion(common)
|
||||||
|
|
||||||
# Start the OnionShare app
|
# Start the OnionShare app
|
||||||
app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout)
|
app = OnionShare(common, onion, local_only)
|
||||||
|
|
||||||
# Launch the gui
|
# Launch the gui
|
||||||
gui = OnionShareGui(common, web, onion, qtapp, app, filenames, config, local_only)
|
gui = OnionShareGui(common, onion, qtapp, app, filenames, config, local_only)
|
||||||
|
|
||||||
# Clean up when app quits
|
# Clean up when app quits
|
||||||
def shutdown():
|
def shutdown():
|
||||||
|
338
onionshare_gui/mode.py
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# -*- 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
|
||||||
|
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 .widgets import Alert
|
||||||
|
|
||||||
|
class Mode(QtWidgets.QWidget):
|
||||||
|
"""
|
||||||
|
The class that ShareMode and ReceiveMode inherit from.
|
||||||
|
"""
|
||||||
|
start_server_finished = QtCore.pyqtSignal()
|
||||||
|
stop_server_finished = QtCore.pyqtSignal()
|
||||||
|
starting_server_step2 = QtCore.pyqtSignal()
|
||||||
|
starting_server_step3 = QtCore.pyqtSignal()
|
||||||
|
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):
|
||||||
|
super(Mode, self).__init__()
|
||||||
|
self.common = common
|
||||||
|
self.qtapp = qtapp
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
self.status_bar = status_bar
|
||||||
|
self.server_status_label = server_status_label
|
||||||
|
self.system_tray = system_tray
|
||||||
|
|
||||||
|
self.filenames = filenames
|
||||||
|
|
||||||
|
self.setMinimumWidth(450)
|
||||||
|
|
||||||
|
# The web object gets created in init()
|
||||||
|
self.web = None
|
||||||
|
|
||||||
|
# Server status
|
||||||
|
self.server_status = ServerStatus(self.common, self.qtapp, self.app)
|
||||||
|
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)
|
||||||
|
self.start_server_finished.connect(self.server_status.start_server_finished)
|
||||||
|
self.stop_server_finished.connect(self.server_status.stop_server_finished)
|
||||||
|
self.starting_server_step2.connect(self.start_server_step2)
|
||||||
|
self.starting_server_step3.connect(self.start_server_step3)
|
||||||
|
self.starting_server_error.connect(self.start_server_error)
|
||||||
|
|
||||||
|
# Primary action layout
|
||||||
|
self.primary_action_layout = QtWidgets.QVBoxLayout()
|
||||||
|
self.primary_action_layout.addWidget(self.server_status)
|
||||||
|
self.primary_action = QtWidgets.QWidget()
|
||||||
|
self.primary_action.setLayout(self.primary_action_layout)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
self.layout = QtWidgets.QVBoxLayout()
|
||||||
|
self.layout.addWidget(self.primary_action)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def timer_callback(self):
|
||||||
|
"""
|
||||||
|
This method is called regularly on a timer.
|
||||||
|
"""
|
||||||
|
# If the auto-shutdown timer has stopped, stop the server
|
||||||
|
if self.server_status.status == ServerStatus.STATUS_STARTED:
|
||||||
|
if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'):
|
||||||
|
if self.timeout > 0:
|
||||||
|
now = QtCore.QDateTime.currentDateTime()
|
||||||
|
seconds_remaining = now.secsTo(self.server_status.timeout)
|
||||||
|
|
||||||
|
# Update the server button
|
||||||
|
server_button_text = self.get_stop_server_shutdown_timeout_text()
|
||||||
|
self.server_status.server_button.setText(server_button_text.format(seconds_remaining))
|
||||||
|
|
||||||
|
self.status_bar.clearMessage()
|
||||||
|
if not self.app.shutdown_timer.is_alive():
|
||||||
|
if self.timeout_finished_should_stop_server():
|
||||||
|
self.server_status.stop_server()
|
||||||
|
|
||||||
|
def timer_callback_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom timer code.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_stop_server_shutdown_timeout_text(self):
|
||||||
|
"""
|
||||||
|
Return the string to put on the stop server button, if there's a shutdown timeout
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def timeout_finished_should_stop_server(self):
|
||||||
|
"""
|
||||||
|
The shutdown timer expired, should we stop the server? Returns a bool
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
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.start_server_custom()
|
||||||
|
|
||||||
|
self.set_server_active.emit(True)
|
||||||
|
self.app.set_stealth(self.common.settings.get('use_stealth'))
|
||||||
|
|
||||||
|
# Clear the status bar
|
||||||
|
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()
|
||||||
|
|
||||||
|
def start_server_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start_server_step2(self):
|
||||||
|
"""
|
||||||
|
Step 2 in starting the onionshare server.
|
||||||
|
"""
|
||||||
|
self.common.log('Mode', 'start_server_step2')
|
||||||
|
|
||||||
|
self.start_server_step2_custom()
|
||||||
|
|
||||||
|
# Nothing to do here.
|
||||||
|
|
||||||
|
# start_server_step2_custom has call these to move on:
|
||||||
|
# self.starting_server_step3.emit()
|
||||||
|
# self.start_server_finished.emit()
|
||||||
|
|
||||||
|
def start_server_step2_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start_server_step3(self):
|
||||||
|
"""
|
||||||
|
Step 3 in starting the onionshare server.
|
||||||
|
"""
|
||||||
|
self.common.log('Mode', 'start_server_step3')
|
||||||
|
|
||||||
|
self.start_server_step3_custom()
|
||||||
|
|
||||||
|
if self.common.settings.get('shutdown_timeout'):
|
||||||
|
# Convert the date value to seconds between now and then
|
||||||
|
now = QtCore.QDateTime.currentDateTime()
|
||||||
|
self.timeout = now.secsTo(self.server_status.timeout)
|
||||||
|
# Set the shutdown timeout value
|
||||||
|
if self.timeout > 0:
|
||||||
|
self.app.shutdown_timer = ShutdownTimer(self.common, self.timeout)
|
||||||
|
self.app.shutdown_timer.start()
|
||||||
|
# The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start.
|
||||||
|
else:
|
||||||
|
self.stop_server()
|
||||||
|
self.start_server_error(strings._('gui_server_started_after_timeout'))
|
||||||
|
|
||||||
|
def start_server_step3_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start_server_error(self, error):
|
||||||
|
"""
|
||||||
|
If there's an error when trying to start the onion service
|
||||||
|
"""
|
||||||
|
self.common.log('Mode', 'start_server_error')
|
||||||
|
|
||||||
|
Alert(self.common, error, QtWidgets.QMessageBox.Warning)
|
||||||
|
self.set_server_active.emit(False)
|
||||||
|
self.server_status.stop_server()
|
||||||
|
self.status_bar.clearMessage()
|
||||||
|
|
||||||
|
self.start_server_error_custom()
|
||||||
|
|
||||||
|
def start_server_error_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cancel_server(self):
|
||||||
|
"""
|
||||||
|
Cancel the server while it is preparing to start
|
||||||
|
"""
|
||||||
|
if self.t:
|
||||||
|
self.t.quit()
|
||||||
|
self.stop_server()
|
||||||
|
|
||||||
|
def stop_server(self):
|
||||||
|
"""
|
||||||
|
Stop the onionshare server.
|
||||||
|
"""
|
||||||
|
self.common.log('Mode', 'stop_server')
|
||||||
|
|
||||||
|
if self.server_status.status != ServerStatus.STATUS_STOPPED:
|
||||||
|
try:
|
||||||
|
self.web.stop(self.app.port)
|
||||||
|
except:
|
||||||
|
# Probably we had no port to begin with (Onion service didn't start)
|
||||||
|
pass
|
||||||
|
self.app.cleanup()
|
||||||
|
|
||||||
|
self.stop_server_custom()
|
||||||
|
|
||||||
|
self.set_server_active.emit(False)
|
||||||
|
self.stop_server_finished.emit()
|
||||||
|
|
||||||
|
def stop_server_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_tor_broke(self):
|
||||||
|
"""
|
||||||
|
Handle connection from Tor breaking.
|
||||||
|
"""
|
||||||
|
if self.server_status.status != ServerStatus.STATUS_STOPPED:
|
||||||
|
self.server_status.stop_server()
|
||||||
|
self.handle_tor_broke_custom()
|
||||||
|
|
||||||
|
def handle_tor_broke_custom(self):
|
||||||
|
"""
|
||||||
|
Add custom initialization here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Handle web server events
|
||||||
|
|
||||||
|
def handle_request_load(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_LOAD event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_started(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_STARTED event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_rate_limit(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_RATE_LIMIT event.
|
||||||
|
"""
|
||||||
|
self.stop_server()
|
||||||
|
Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
|
||||||
|
|
||||||
|
def handle_request_progress(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_PROGRESS event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_canceled(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_CANCELED event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_close_server(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_CLOSE_SERVER event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_upload_file_renamed(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_UPLOAD_FILE_RENAMED event.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_request_upload_finished(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_UPLOAD_FINISHED event.
|
||||||
|
"""
|
||||||
|
pass
|
45
onionshare_gui/onion_thread.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- 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)
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -17,138 +17,87 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import queue
|
import queue
|
||||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
from onionshare import strings, common
|
from onionshare import strings
|
||||||
from onionshare.common import Common, ShutdownTimer
|
from onionshare.web import Web
|
||||||
from onionshare.onion import *
|
|
||||||
|
from .share_mode import ShareMode
|
||||||
|
from .receive_mode import ReceiveMode
|
||||||
|
|
||||||
from .tor_connection_dialog import TorConnectionDialog
|
from .tor_connection_dialog import TorConnectionDialog
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
from .file_selection import FileSelection
|
from .widgets import Alert
|
||||||
from .server_status import ServerStatus
|
|
||||||
from .downloads import Downloads
|
|
||||||
from .alert import Alert
|
|
||||||
from .update_checker import UpdateThread
|
from .update_checker import UpdateThread
|
||||||
|
from .server_status import ServerStatus
|
||||||
|
|
||||||
class OnionShareGui(QtWidgets.QMainWindow):
|
class OnionShareGui(QtWidgets.QMainWindow):
|
||||||
"""
|
"""
|
||||||
OnionShareGui is the main window for the GUI that contains all of the
|
OnionShareGui is the main window for the GUI that contains all of the
|
||||||
GUI elements.
|
GUI elements.
|
||||||
"""
|
"""
|
||||||
start_server_finished = QtCore.pyqtSignal()
|
MODE_SHARE = 'share'
|
||||||
stop_server_finished = QtCore.pyqtSignal()
|
MODE_RECEIVE = 'receive'
|
||||||
starting_server_step2 = QtCore.pyqtSignal()
|
|
||||||
starting_server_step3 = QtCore.pyqtSignal()
|
|
||||||
starting_server_error = QtCore.pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, common, web, onion, qtapp, app, filenames, config=False, local_only=False):
|
def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False):
|
||||||
super(OnionShareGui, self).__init__()
|
super(OnionShareGui, self).__init__()
|
||||||
|
|
||||||
self.common = common
|
self.common = common
|
||||||
self.common.log('OnionShareGui', '__init__')
|
self.common.log('OnionShareGui', '__init__')
|
||||||
|
|
||||||
self._initSystemTray()
|
|
||||||
|
|
||||||
self.web = web
|
|
||||||
self.onion = onion
|
self.onion = onion
|
||||||
self.qtapp = qtapp
|
self.qtapp = qtapp
|
||||||
self.app = app
|
self.app = app
|
||||||
self.local_only = local_only
|
self.local_only = local_only
|
||||||
|
|
||||||
|
self.mode = self.MODE_SHARE
|
||||||
|
|
||||||
self.setWindowTitle('OnionShare')
|
self.setWindowTitle('OnionShare')
|
||||||
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
|
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
|
||||||
self.setMinimumWidth(430)
|
self.setMinimumWidth(450)
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
self.config = config
|
self.config = config
|
||||||
self.common.load_settings(self.config)
|
self.common.load_settings(self.config)
|
||||||
|
|
||||||
# File selection
|
# System tray
|
||||||
self.file_selection = FileSelection(self.common)
|
menu = QtWidgets.QMenu()
|
||||||
if filenames:
|
self.settings_action = menu.addAction(strings._('gui_settings_window_title', True))
|
||||||
for filename in filenames:
|
self.settings_action.triggered.connect(self.open_settings)
|
||||||
self.file_selection.file_list.add_file(filename)
|
help_action = menu.addAction(strings._('gui_settings_button_help', True))
|
||||||
|
help_action.triggered.connect(SettingsDialog.help_clicked)
|
||||||
|
exit_action = menu.addAction(strings._('systray_menu_exit', True))
|
||||||
|
exit_action.triggered.connect(self.close)
|
||||||
|
|
||||||
# Server status
|
self.system_tray = QtWidgets.QSystemTrayIcon(self)
|
||||||
self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection)
|
# The convention is Mac systray icons are always grayscale
|
||||||
self.server_status.server_started.connect(self.file_selection.server_started)
|
if self.common.platform == 'Darwin':
|
||||||
self.server_status.server_started.connect(self.start_server)
|
self.system_tray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo_grayscale.png')))
|
||||||
self.server_status.server_started.connect(self.update_server_status_indicator)
|
else:
|
||||||
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
|
self.system_tray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
|
||||||
self.server_status.server_stopped.connect(self.stop_server)
|
self.system_tray.setContextMenu(menu)
|
||||||
self.server_status.server_stopped.connect(self.update_server_status_indicator)
|
self.system_tray.show()
|
||||||
self.server_status.server_stopped.connect(self.update_primary_action)
|
|
||||||
self.server_status.server_canceled.connect(self.cancel_server)
|
|
||||||
self.server_status.server_canceled.connect(self.file_selection.server_stopped)
|
|
||||||
self.server_status.server_canceled.connect(self.update_primary_action)
|
|
||||||
self.start_server_finished.connect(self.clear_message)
|
|
||||||
self.start_server_finished.connect(self.server_status.start_server_finished)
|
|
||||||
self.start_server_finished.connect(self.update_server_status_indicator)
|
|
||||||
self.stop_server_finished.connect(self.server_status.stop_server_finished)
|
|
||||||
self.stop_server_finished.connect(self.update_server_status_indicator)
|
|
||||||
self.file_selection.file_list.files_updated.connect(self.server_status.update)
|
|
||||||
self.file_selection.file_list.files_updated.connect(self.update_primary_action)
|
|
||||||
self.server_status.url_copied.connect(self.copy_url)
|
|
||||||
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
|
|
||||||
self.starting_server_step2.connect(self.start_server_step2)
|
|
||||||
self.starting_server_step3.connect(self.start_server_step3)
|
|
||||||
self.starting_server_error.connect(self.start_server_error)
|
|
||||||
self.server_status.button_clicked.connect(self.clear_message)
|
|
||||||
|
|
||||||
# Filesize warning
|
# Mode switcher, to switch between share files and receive files
|
||||||
self.filesize_warning = QtWidgets.QLabel()
|
self.share_mode_button = QtWidgets.QPushButton(strings._('gui_mode_share_button', True));
|
||||||
self.filesize_warning.setWordWrap(True)
|
self.share_mode_button.setFixedHeight(50)
|
||||||
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
|
self.share_mode_button.clicked.connect(self.share_mode_clicked)
|
||||||
self.filesize_warning.hide()
|
self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button', True));
|
||||||
|
self.receive_mode_button.setFixedHeight(50)
|
||||||
# Downloads
|
self.receive_mode_button.clicked.connect(self.receive_mode_clicked)
|
||||||
self.downloads = Downloads(self.common)
|
|
||||||
self.new_download = False
|
|
||||||
self.downloads_in_progress = 0
|
|
||||||
self.downloads_completed = 0
|
|
||||||
|
|
||||||
# Info label along top of screen
|
|
||||||
self.info_layout = QtWidgets.QHBoxLayout()
|
|
||||||
self.info_label = QtWidgets.QLabel()
|
|
||||||
self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
|
|
||||||
|
|
||||||
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('QLabel { font-size: 12px; color: #666666; }')
|
|
||||||
|
|
||||||
self.info_completed_downloads_count = QtWidgets.QLabel()
|
|
||||||
self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
|
|
||||||
|
|
||||||
self.update_downloads_completed(self.downloads_in_progress)
|
|
||||||
self.update_downloads_in_progress(self.downloads_in_progress)
|
|
||||||
|
|
||||||
self.info_layout.addWidget(self.info_label)
|
|
||||||
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)
|
|
||||||
self.info_widget.hide()
|
|
||||||
|
|
||||||
# Settings button on the status bar
|
|
||||||
self.settings_button = QtWidgets.QPushButton()
|
self.settings_button = QtWidgets.QPushButton()
|
||||||
self.settings_button.setDefault(False)
|
self.settings_button.setDefault(False)
|
||||||
self.settings_button.setFlat(True)
|
|
||||||
self.settings_button.setFixedWidth(40)
|
self.settings_button.setFixedWidth(40)
|
||||||
|
self.settings_button.setFixedHeight(50)
|
||||||
self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/settings.png')) )
|
self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/settings.png')) )
|
||||||
self.settings_button.clicked.connect(self.open_settings)
|
self.settings_button.clicked.connect(self.open_settings)
|
||||||
|
self.settings_button.setStyleSheet(self.common.css['settings_button'])
|
||||||
|
mode_switcher_layout = QtWidgets.QHBoxLayout();
|
||||||
|
mode_switcher_layout.setSpacing(0)
|
||||||
|
mode_switcher_layout.addWidget(self.share_mode_button)
|
||||||
|
mode_switcher_layout.addWidget(self.receive_mode_button)
|
||||||
|
mode_switcher_layout.addWidget(self.settings_button)
|
||||||
|
|
||||||
# Server status indicator on the status bar
|
# Server status indicator on the status bar
|
||||||
self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png'))
|
self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png'))
|
||||||
@ -156,68 +105,74 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png'))
|
self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png'))
|
||||||
self.server_status_image_label = QtWidgets.QLabel()
|
self.server_status_image_label = QtWidgets.QLabel()
|
||||||
self.server_status_image_label.setFixedWidth(20)
|
self.server_status_image_label.setFixedWidth(20)
|
||||||
self.server_status_label = QtWidgets.QLabel()
|
self.server_status_label = QtWidgets.QLabel('')
|
||||||
self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }')
|
self.server_status_label.setStyleSheet(self.common.css['server_status_indicator_label'])
|
||||||
server_status_indicator_layout = QtWidgets.QHBoxLayout()
|
server_status_indicator_layout = QtWidgets.QHBoxLayout()
|
||||||
server_status_indicator_layout.addWidget(self.server_status_image_label)
|
server_status_indicator_layout.addWidget(self.server_status_image_label)
|
||||||
server_status_indicator_layout.addWidget(self.server_status_label)
|
server_status_indicator_layout.addWidget(self.server_status_label)
|
||||||
self.server_status_indicator = QtWidgets.QWidget()
|
self.server_status_indicator = QtWidgets.QWidget()
|
||||||
self.server_status_indicator.setLayout(server_status_indicator_layout)
|
self.server_status_indicator.setLayout(server_status_indicator_layout)
|
||||||
self.update_server_status_indicator()
|
|
||||||
|
|
||||||
# Status bar
|
# Status bar
|
||||||
self.status_bar = QtWidgets.QStatusBar()
|
self.status_bar = QtWidgets.QStatusBar()
|
||||||
self.status_bar.setSizeGripEnabled(False)
|
self.status_bar.setSizeGripEnabled(False)
|
||||||
statusBar_cssStyleData ="""
|
self.status_bar.setStyleSheet(self.common.css['status_bar'])
|
||||||
QStatusBar {
|
|
||||||
font-style: italic;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStatusBar::item {
|
|
||||||
border: 0px;
|
|
||||||
}"""
|
|
||||||
|
|
||||||
self.status_bar.setStyleSheet(statusBar_cssStyleData)
|
|
||||||
self.status_bar.addPermanentWidget(self.server_status_indicator)
|
self.status_bar.addPermanentWidget(self.server_status_indicator)
|
||||||
self.status_bar.addPermanentWidget(self.settings_button)
|
|
||||||
self.setStatusBar(self.status_bar)
|
self.setStatusBar(self.status_bar)
|
||||||
|
|
||||||
# Status bar, zip progress bar
|
# Share mode
|
||||||
self._zip_progress_bar = None
|
self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames)
|
||||||
# Status bar, sharing messages
|
self.share_mode.init()
|
||||||
self.server_share_status_label = QtWidgets.QLabel('')
|
self.share_mode.server_status.server_started.connect(self.update_server_status_indicator)
|
||||||
self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }')
|
self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
|
||||||
self.status_bar.insertWidget(0, self.server_share_status_label)
|
self.share_mode.start_server_finished.connect(self.update_server_status_indicator)
|
||||||
|
self.share_mode.stop_server_finished.connect(self.update_server_status_indicator)
|
||||||
|
self.share_mode.stop_server_finished.connect(self.stop_server_finished)
|
||||||
|
self.share_mode.start_server_finished.connect(self.clear_message)
|
||||||
|
self.share_mode.server_status.button_clicked.connect(self.clear_message)
|
||||||
|
self.share_mode.server_status.url_copied.connect(self.copy_url)
|
||||||
|
self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
|
||||||
|
self.share_mode.set_server_active.connect(self.set_server_active)
|
||||||
|
|
||||||
# Primary action layout
|
# Receive mode
|
||||||
primary_action_layout = QtWidgets.QVBoxLayout()
|
self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray)
|
||||||
primary_action_layout.addWidget(self.server_status)
|
self.receive_mode.init()
|
||||||
primary_action_layout.addWidget(self.filesize_warning)
|
self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator)
|
||||||
self.primary_action = QtWidgets.QWidget()
|
self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
|
||||||
self.primary_action.setLayout(primary_action_layout)
|
self.receive_mode.start_server_finished.connect(self.update_server_status_indicator)
|
||||||
self.primary_action.hide()
|
self.receive_mode.stop_server_finished.connect(self.update_server_status_indicator)
|
||||||
self.update_primary_action()
|
self.receive_mode.stop_server_finished.connect(self.stop_server_finished)
|
||||||
|
self.receive_mode.start_server_finished.connect(self.clear_message)
|
||||||
|
self.receive_mode.server_status.button_clicked.connect(self.clear_message)
|
||||||
|
self.receive_mode.server_status.url_copied.connect(self.copy_url)
|
||||||
|
self.receive_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
|
||||||
|
self.receive_mode.set_server_active.connect(self.set_server_active)
|
||||||
|
|
||||||
|
self.update_mode_switcher()
|
||||||
|
self.update_server_status_indicator()
|
||||||
|
|
||||||
|
# Layouts
|
||||||
|
contents_layout = QtWidgets.QVBoxLayout()
|
||||||
|
contents_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
contents_layout.addWidget(self.receive_mode)
|
||||||
|
contents_layout.addWidget(self.share_mode)
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.addLayout(mode_switcher_layout)
|
||||||
|
layout.addLayout(contents_layout)
|
||||||
|
|
||||||
# Main layout
|
|
||||||
self.layout = QtWidgets.QVBoxLayout()
|
|
||||||
self.layout.addWidget(self.info_widget)
|
|
||||||
self.layout.addLayout(self.file_selection)
|
|
||||||
self.layout.addWidget(self.primary_action)
|
|
||||||
central_widget = QtWidgets.QWidget()
|
central_widget = QtWidgets.QWidget()
|
||||||
central_widget.setLayout(self.layout)
|
central_widget.setLayout(layout)
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
# Always start with focus on file selection
|
|
||||||
self.file_selection.setFocus()
|
|
||||||
|
|
||||||
# The server isn't active yet
|
# The server isn't active yet
|
||||||
self.set_server_active(False)
|
self.set_server_active(False)
|
||||||
|
|
||||||
# Create the timer
|
# Create the timer
|
||||||
self.timer = QtCore.QTimer()
|
self.timer = QtCore.QTimer()
|
||||||
self.timer.timeout.connect(self.check_for_requests)
|
self.timer.timeout.connect(self.timer_callback)
|
||||||
|
|
||||||
# Start the "Connecting to Tor" dialog, which calls onion.connect()
|
# Start the "Connecting to Tor" dialog, which calls onion.connect()
|
||||||
tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion)
|
tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion)
|
||||||
@ -232,63 +187,67 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
# After connecting to Tor, check for updates
|
# After connecting to Tor, check for updates
|
||||||
self.check_for_updates()
|
self.check_for_updates()
|
||||||
|
|
||||||
def update_primary_action(self):
|
def update_mode_switcher(self):
|
||||||
# Show or hide primary action layout
|
# Based on the current mode, switch the mode switcher button styles,
|
||||||
file_count = self.file_selection.file_list.count()
|
# and show and hide widgets to switch modes
|
||||||
if file_count > 0:
|
if self.mode == self.MODE_SHARE:
|
||||||
self.primary_action.show()
|
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
|
||||||
self.info_widget.show()
|
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
|
||||||
|
|
||||||
# Update the file count in the info label
|
self.share_mode.show()
|
||||||
total_size_bytes = 0
|
self.receive_mode.hide()
|
||||||
for index in range(self.file_selection.file_list.count()):
|
|
||||||
item = self.file_selection.file_list.item(index)
|
|
||||||
total_size_bytes += item.size_bytes
|
|
||||||
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
|
|
||||||
|
|
||||||
if file_count > 1:
|
|
||||||
self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
|
|
||||||
else:
|
else:
|
||||||
self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
|
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
|
||||||
|
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
|
||||||
|
|
||||||
else:
|
self.share_mode.hide()
|
||||||
self.primary_action.hide()
|
self.receive_mode.show()
|
||||||
self.info_widget.hide()
|
|
||||||
|
|
||||||
# Resize window
|
self.update_server_status_indicator()
|
||||||
self.adjustSize()
|
|
||||||
|
# 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')
|
||||||
|
self.mode = self.MODE_SHARE
|
||||||
|
self.update_mode_switcher()
|
||||||
|
|
||||||
|
def receive_mode_clicked(self):
|
||||||
|
if self.mode != self.MODE_RECEIVE:
|
||||||
|
self.common.log('OnionShareGui', 'receive_mode_clicked')
|
||||||
|
self.mode = self.MODE_RECEIVE
|
||||||
|
self.update_mode_switcher()
|
||||||
|
|
||||||
def update_server_status_indicator(self):
|
def update_server_status_indicator(self):
|
||||||
self.common.log('OnionShareGui', 'update_server_status_indicator')
|
|
||||||
|
|
||||||
# Set the status image
|
# Set the status image
|
||||||
if self.server_status.status == self.server_status.STATUS_STOPPED:
|
if self.mode == self.MODE_SHARE:
|
||||||
|
# Share mode
|
||||||
|
if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED:
|
||||||
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
|
||||||
self.server_status_label.setText(strings._('gui_status_indicator_stopped', True))
|
self.server_status_label.setText(strings._('gui_status_indicator_share_stopped', True))
|
||||||
elif self.server_status.status == self.server_status.STATUS_WORKING:
|
elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING:
|
||||||
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
|
||||||
self.server_status_label.setText(strings._('gui_status_indicator_working', True))
|
self.server_status_label.setText(strings._('gui_status_indicator_share_working', True))
|
||||||
elif self.server_status.status == self.server_status.STATUS_STARTED:
|
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED:
|
||||||
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
|
||||||
self.server_status_label.setText(strings._('gui_status_indicator_started', True))
|
self.server_status_label.setText(strings._('gui_status_indicator_share_started', True))
|
||||||
|
|
||||||
def _initSystemTray(self):
|
|
||||||
menu = QtWidgets.QMenu()
|
|
||||||
self.settingsAction = menu.addAction(strings._('gui_settings_window_title', True))
|
|
||||||
self.settingsAction.triggered.connect(self.open_settings)
|
|
||||||
self.helpAction = menu.addAction(strings._('gui_settings_button_help', True))
|
|
||||||
self.helpAction.triggered.connect(lambda: SettingsDialog.help_clicked(self))
|
|
||||||
self.exitAction = menu.addAction(strings._('systray_menu_exit', True))
|
|
||||||
self.exitAction.triggered.connect(self.close)
|
|
||||||
|
|
||||||
self.systemTray = QtWidgets.QSystemTrayIcon(self)
|
|
||||||
# The convention is Mac systray icons are always grayscale
|
|
||||||
if self.common.platform == 'Darwin':
|
|
||||||
self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo_grayscale.png')))
|
|
||||||
else:
|
else:
|
||||||
self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
|
# Receive mode
|
||||||
self.systemTray.setContextMenu(menu)
|
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
|
||||||
self.systemTray.show()
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
|
||||||
|
self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped', True))
|
||||||
|
elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING:
|
||||||
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
|
||||||
|
self.server_status_label.setText(strings._('gui_status_indicator_receive_working', True))
|
||||||
|
elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED:
|
||||||
|
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
|
||||||
|
self.server_status_label.setText(strings._('gui_status_indicator_receive_started', True))
|
||||||
|
|
||||||
|
def stop_server_finished(self):
|
||||||
|
# When the server stopped, cleanup the ephemeral onion service
|
||||||
|
self.onion.cleanup(stop_tor=False)
|
||||||
|
|
||||||
def _tor_connection_canceled(self):
|
def _tor_connection_canceled(self):
|
||||||
"""
|
"""
|
||||||
@ -339,6 +298,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
def reload_settings():
|
def reload_settings():
|
||||||
self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading')
|
self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading')
|
||||||
self.common.settings.load()
|
self.common.settings.load()
|
||||||
|
|
||||||
# We might've stopped the main requests timer if a Tor connection failed.
|
# We might've stopped the main requests timer if a Tor connection failed.
|
||||||
# If we've reloaded settings, we probably succeeded in obtaining a new
|
# If we've reloaded settings, we probably succeeded in obtaining a new
|
||||||
# connection. If so, restart the timer.
|
# connection. If so, restart the timer.
|
||||||
@ -346,181 +306,22 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
if self.onion.is_authenticated():
|
if self.onion.is_authenticated():
|
||||||
if not self.timer.isActive():
|
if not self.timer.isActive():
|
||||||
self.timer.start(500)
|
self.timer.start(500)
|
||||||
# If there were some files listed for sharing, we should be ok to
|
self.share_mode.on_reload_settings()
|
||||||
# re-enable the 'Start Sharing' button now.
|
self.receive_mode.on_reload_settings()
|
||||||
if self.server_status.file_selection.get_num_files() > 0:
|
|
||||||
self.primary_action.show()
|
|
||||||
self.info_widget.show()
|
|
||||||
self.status_bar.clearMessage()
|
self.status_bar.clearMessage()
|
||||||
|
|
||||||
# If we switched off the shutdown timeout setting, ensure the widget is hidden.
|
# If we switched off the shutdown timeout setting, ensure the widget is hidden.
|
||||||
if not self.common.settings.get('shutdown_timeout'):
|
if not self.common.settings.get('shutdown_timeout'):
|
||||||
self.server_status.shutdown_timeout_container.hide()
|
self.share_mode.server_status.shutdown_timeout_container.hide()
|
||||||
|
self.receive_mode.server_status.shutdown_timeout_container.hide()
|
||||||
|
|
||||||
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
|
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
|
||||||
d.settings_saved.connect(reload_settings)
|
d.settings_saved.connect(reload_settings)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
# When settings close, refresh the server status UI
|
# When settings close, refresh the server status UI
|
||||||
self.server_status.update()
|
self.share_mode.server_status.update()
|
||||||
|
self.receive_mode.server_status.update()
|
||||||
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('OnionShareGui', 'start_server')
|
|
||||||
|
|
||||||
self.set_server_active(True)
|
|
||||||
|
|
||||||
self.app.set_stealth(self.common.settings.get('use_stealth'))
|
|
||||||
|
|
||||||
# Hide and reset the downloads if we have previously shared
|
|
||||||
self.downloads.reset_downloads()
|
|
||||||
self.reset_info_counters()
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
self.server_share_status_label.setText('')
|
|
||||||
|
|
||||||
# Reset web counters
|
|
||||||
self.web.download_count = 0
|
|
||||||
self.web.error404_count = 0
|
|
||||||
|
|
||||||
# start the onion service in a new thread
|
|
||||||
def start_onion_service(self):
|
|
||||||
try:
|
|
||||||
self.app.start_onion_service()
|
|
||||||
self.starting_server_step2.emit()
|
|
||||||
|
|
||||||
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e:
|
|
||||||
self.starting_server_error.emit(e.args[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
self.app.stay_open = not self.common.settings.get('close_after_first_download')
|
|
||||||
|
|
||||||
# start onionshare http service in new thread
|
|
||||||
t = threading.Thread(target=self.web.start, args=(self.app.port, self.app.stay_open, self.common.settings.get('slug')))
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
self.common.log('OnionshareGui', '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()
|
|
||||||
|
|
||||||
def start_server_step2(self):
|
|
||||||
"""
|
|
||||||
Step 2 in starting the onionshare server. Zipping up files.
|
|
||||||
"""
|
|
||||||
self.common.log('OnionShareGui', 'start_server_step2')
|
|
||||||
|
|
||||||
# add progress bar to the status bar, indicating the compressing of files.
|
|
||||||
self._zip_progress_bar = ZipProgressBar(0)
|
|
||||||
self.filenames = []
|
|
||||||
for index in range(self.file_selection.file_list.count()):
|
|
||||||
self.filenames.append(self.file_selection.file_list.item(index).filename)
|
|
||||||
|
|
||||||
self._zip_progress_bar.total_files_size = OnionShareGui._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)
|
|
||||||
self.starting_server_step3.emit()
|
|
||||||
|
|
||||||
# done
|
|
||||||
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()
|
|
||||||
|
|
||||||
def start_server_step3(self):
|
|
||||||
"""
|
|
||||||
Step 3 in starting the onionshare server. This displays the large filesize
|
|
||||||
warning, if applicable.
|
|
||||||
"""
|
|
||||||
self.common.log('OnionShareGui', 'start_server_step3')
|
|
||||||
|
|
||||||
# Remove zip progress bar
|
|
||||||
if self._zip_progress_bar is not None:
|
|
||||||
self.status_bar.removeWidget(self._zip_progress_bar)
|
|
||||||
self._zip_progress_bar = None
|
|
||||||
|
|
||||||
# warn about sending large files over Tor
|
|
||||||
if self.web.zip_filesize >= 157286400: # 150mb
|
|
||||||
self.filesize_warning.setText(strings._("large_filesize", True))
|
|
||||||
self.filesize_warning.show()
|
|
||||||
|
|
||||||
if self.common.settings.get('shutdown_timeout'):
|
|
||||||
# Convert the date value to seconds between now and then
|
|
||||||
now = QtCore.QDateTime.currentDateTime()
|
|
||||||
self.timeout = now.secsTo(self.server_status.timeout)
|
|
||||||
# Set the shutdown timeout value
|
|
||||||
if self.timeout > 0:
|
|
||||||
self.app.shutdown_timer = ShutdownTimer(self.common, self.timeout)
|
|
||||||
self.app.shutdown_timer.start()
|
|
||||||
# The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start.
|
|
||||||
else:
|
|
||||||
self.stop_server()
|
|
||||||
self.start_server_error(strings._('gui_server_started_after_timeout'))
|
|
||||||
|
|
||||||
def start_server_error(self, error):
|
|
||||||
"""
|
|
||||||
If there's an error when trying to start the onion service
|
|
||||||
"""
|
|
||||||
self.common.log('OnionShareGui', 'start_server_error')
|
|
||||||
|
|
||||||
self.set_server_active(False)
|
|
||||||
|
|
||||||
Alert(self.common, error, QtWidgets.QMessageBox.Warning)
|
|
||||||
self.server_status.stop_server()
|
|
||||||
if self._zip_progress_bar is not None:
|
|
||||||
self.status_bar.removeWidget(self._zip_progress_bar)
|
|
||||||
self._zip_progress_bar = None
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
|
|
||||||
def cancel_server(self):
|
|
||||||
"""
|
|
||||||
Cancel the server while it is preparing to start
|
|
||||||
"""
|
|
||||||
if self.t:
|
|
||||||
self.t.quit()
|
|
||||||
self.stop_server()
|
|
||||||
|
|
||||||
def stop_server(self):
|
|
||||||
"""
|
|
||||||
Stop the onionshare server.
|
|
||||||
"""
|
|
||||||
self.common.log('OnionShareGui', 'stop_server')
|
|
||||||
|
|
||||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
|
||||||
try:
|
|
||||||
self.web.stop(self.app.port)
|
|
||||||
except:
|
|
||||||
# Probably we had no port to begin with (Onion service didn't start)
|
|
||||||
pass
|
|
||||||
self.app.cleanup()
|
|
||||||
# Remove ephemeral service, but don't disconnect from Tor
|
|
||||||
self.onion.cleanup(stop_tor=False)
|
|
||||||
self.filesize_warning.hide()
|
|
||||||
self.downloads_in_progress = 0
|
|
||||||
self.downloads_completed = 0
|
|
||||||
self.update_downloads_in_progress(0)
|
|
||||||
self.file_selection.file_list.adjustSize()
|
|
||||||
|
|
||||||
self.set_server_active(False)
|
|
||||||
self.stop_server_finished.emit()
|
|
||||||
|
|
||||||
def check_for_updates(self):
|
def check_for_updates(self):
|
||||||
"""
|
"""
|
||||||
@ -535,19 +336,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
self.update_thread.update_available.connect(update_available)
|
self.update_thread.update_available.connect(update_available)
|
||||||
self.update_thread.start()
|
self.update_thread.start()
|
||||||
|
|
||||||
@staticmethod
|
def timer_callback(self):
|
||||||
def _compute_total_size(filenames):
|
|
||||||
total_size = 0
|
|
||||||
for filename in filenames:
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
total_size += os.path.getsize(filename)
|
|
||||||
if os.path.isdir(filename):
|
|
||||||
total_size += Common.dir_size(filename)
|
|
||||||
return total_size
|
|
||||||
|
|
||||||
def check_for_requests(self):
|
|
||||||
"""
|
"""
|
||||||
Check for messages communicated from the web app, and update the GUI accordingly.
|
Check for messages communicated from the web app, and update the GUI accordingly. Also,
|
||||||
|
call ShareMode and ReceiveMode's timer_callbacks.
|
||||||
"""
|
"""
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@ -555,127 +347,78 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
# Have we lost connection to Tor somehow?
|
# Have we lost connection to Tor somehow?
|
||||||
if not self.onion.is_authenticated():
|
if not self.onion.is_authenticated():
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
|
||||||
self.server_status.stop_server()
|
|
||||||
self.primary_action.hide()
|
|
||||||
self.info_widget.hide()
|
|
||||||
self.status_bar.showMessage(strings._('gui_tor_connection_lost', True))
|
self.status_bar.showMessage(strings._('gui_tor_connection_lost', True))
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
self.system_tray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True))
|
||||||
self.systemTray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True))
|
|
||||||
|
|
||||||
# scroll to the bottom of the dl progress bar log pane
|
self.share_mode.handle_tor_broke()
|
||||||
# if a new download has been added
|
self.receive_mode.handle_tor_broke()
|
||||||
if self.new_download:
|
|
||||||
self.downloads.downloads_container.vbar.setValue(self.downloads.downloads_container.vbar.maximum())
|
# Process events from the web object
|
||||||
self.new_download = False
|
if self.mode == self.MODE_SHARE:
|
||||||
|
mode = self.share_mode
|
||||||
|
else:
|
||||||
|
mode = self.receive_mode
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
done = False
|
done = False
|
||||||
while not done:
|
while not done:
|
||||||
try:
|
try:
|
||||||
r = self.web.q.get(False)
|
r = mode.web.q.get(False)
|
||||||
events.append(r)
|
events.append(r)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
done = True
|
done = True
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
if event["type"] == self.web.REQUEST_LOAD:
|
if event["type"] == Web.REQUEST_LOAD:
|
||||||
self.status_bar.showMessage(strings._('download_page_loaded', True))
|
mode.handle_request_load(event)
|
||||||
|
|
||||||
elif event["type"] == self.web.REQUEST_DOWNLOAD:
|
elif event["type"] == Web.REQUEST_STARTED:
|
||||||
self.downloads.no_downloads_label.hide()
|
mode.handle_request_started(event)
|
||||||
self.downloads.add_download(event["data"]["id"], self.web.zip_filesize)
|
|
||||||
self.new_download = True
|
|
||||||
self.downloads_in_progress += 1
|
|
||||||
self.update_downloads_in_progress(self.downloads_in_progress)
|
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
|
||||||
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
|
|
||||||
|
|
||||||
elif event["type"] == self.web.REQUEST_RATE_LIMIT:
|
elif event["type"] == Web.REQUEST_RATE_LIMIT:
|
||||||
self.stop_server()
|
mode.handle_request_rate_limit(event)
|
||||||
Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
|
|
||||||
|
|
||||||
elif event["type"] == self.web.REQUEST_PROGRESS:
|
elif event["type"] == Web.REQUEST_PROGRESS:
|
||||||
self.downloads.update_download(event["data"]["id"], event["data"]["bytes"])
|
mode.handle_request_progress(event)
|
||||||
|
|
||||||
# is the download complete?
|
elif event["type"] == Web.REQUEST_CANCELED:
|
||||||
if event["data"]["bytes"] == self.web.zip_filesize:
|
mode.handle_request_canceled(event)
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
|
||||||
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
|
|
||||||
# Update the total 'completed downloads' info
|
|
||||||
self.downloads_completed += 1
|
|
||||||
self.update_downloads_completed(self.downloads_completed)
|
|
||||||
# Update the 'in progress downloads' info
|
|
||||||
self.downloads_in_progress -= 1
|
|
||||||
self.update_downloads_in_progress(self.downloads_in_progress)
|
|
||||||
|
|
||||||
# close on finish?
|
elif event["type"] == Web.REQUEST_CLOSE_SERVER:
|
||||||
if not self.web.stay_open:
|
mode.handle_request_close_server(event)
|
||||||
self.server_status.stop_server()
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
self.server_share_status_label.setText(strings._('closing_automatically', True))
|
|
||||||
else:
|
|
||||||
if self.server_status.status == self.server_status.STATUS_STOPPED:
|
|
||||||
self.downloads.cancel_download(event["data"]["id"])
|
|
||||||
self.downloads_in_progress = 0
|
|
||||||
self.update_downloads_in_progress(self.downloads_in_progress)
|
|
||||||
|
|
||||||
|
elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED:
|
||||||
|
mode.handle_request_upload_file_renamed(event)
|
||||||
|
|
||||||
elif event["type"] == self.web.REQUEST_CANCELED:
|
elif event["type"] == Web.REQUEST_UPLOAD_FINISHED:
|
||||||
self.downloads.cancel_download(event["data"]["id"])
|
mode.handle_request_upload_finished(event)
|
||||||
# Update the 'in progress downloads' info
|
|
||||||
self.downloads_in_progress -= 1
|
|
||||||
self.update_downloads_in_progress(self.downloads_in_progress)
|
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
|
||||||
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
|
|
||||||
|
|
||||||
elif event["path"] != '/favicon.ico':
|
if event["type"] == Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE:
|
||||||
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(self.web.error404_count, strings._('other_page_loaded', True), event["path"]))
|
Alert(self.common, strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
|
||||||
|
|
||||||
# If the auto-shutdown timer has stopped, stop the server
|
if event["type"] == Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE:
|
||||||
if self.server_status.status == self.server_status.STATUS_STARTED:
|
Alert(self.common, strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
|
||||||
if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'):
|
|
||||||
if self.timeout > 0:
|
|
||||||
now = QtCore.QDateTime.currentDateTime()
|
|
||||||
seconds_remaining = now.secsTo(self.server_status.timeout)
|
|
||||||
self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
|
|
||||||
if not self.app.shutdown_timer.is_alive():
|
|
||||||
# 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:
|
|
||||||
self.server_status.stop_server()
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
self.server_share_status_label.setText(strings._('close_on_timeout', True))
|
|
||||||
# A download is probably still running - hold off on stopping the share
|
|
||||||
else:
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
|
|
||||||
|
|
||||||
def downloads_toggled(self, checked):
|
if event["type"] == Web.REQUEST_OTHER:
|
||||||
"""
|
if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug):
|
||||||
When the 'Show/hide downloads' button is toggled, show or hide the downloads window.
|
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded', True), event["path"]))
|
||||||
"""
|
|
||||||
self.common.log('OnionShareGui', 'toggle_downloads')
|
mode.timer_callback()
|
||||||
if checked:
|
|
||||||
self.downloads.downloads_container.show()
|
|
||||||
else:
|
|
||||||
self.downloads.downloads_container.hide()
|
|
||||||
|
|
||||||
def copy_url(self):
|
def copy_url(self):
|
||||||
"""
|
"""
|
||||||
When the URL gets copied to the clipboard, display this in the status bar.
|
When the URL gets copied to the clipboard, display this in the status bar.
|
||||||
"""
|
"""
|
||||||
self.common.log('OnionShareGui', 'copy_url')
|
self.common.log('OnionShareGui', 'copy_url')
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
self.system_tray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
|
||||||
self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
|
|
||||||
|
|
||||||
def copy_hidservauth(self):
|
def copy_hidservauth(self):
|
||||||
"""
|
"""
|
||||||
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
|
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
|
||||||
"""
|
"""
|
||||||
self.common.log('OnionShareGui', 'copy_hidservauth')
|
self.common.log('OnionShareGui', 'copy_hidservauth')
|
||||||
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
|
self.system_tray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
|
||||||
self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
|
|
||||||
|
|
||||||
def clear_message(self):
|
def clear_message(self):
|
||||||
"""
|
"""
|
||||||
@ -685,57 +428,39 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
def set_server_active(self, active):
|
def set_server_active(self, active):
|
||||||
"""
|
"""
|
||||||
Disable the Settings button while an OnionShare server is active.
|
Disable the Settings and Receive Files buttons while an Share Files server is active.
|
||||||
"""
|
"""
|
||||||
if active:
|
if active:
|
||||||
self.settings_button.hide()
|
self.settings_button.hide()
|
||||||
|
if self.mode == self.MODE_SHARE:
|
||||||
|
self.share_mode_button.show()
|
||||||
|
self.receive_mode_button.hide()
|
||||||
|
else:
|
||||||
|
self.share_mode_button.hide()
|
||||||
|
self.receive_mode_button.show()
|
||||||
else:
|
else:
|
||||||
self.settings_button.show()
|
self.settings_button.show()
|
||||||
|
self.share_mode_button.show()
|
||||||
|
self.receive_mode_button.show()
|
||||||
|
|
||||||
# Disable settings menu action when server is active
|
# Disable settings menu action when server is active
|
||||||
self.settingsAction.setEnabled(not active)
|
self.settings_action.setEnabled(not active)
|
||||||
|
|
||||||
def reset_info_counters(self):
|
|
||||||
"""
|
|
||||||
Set the info counters back to zero.
|
|
||||||
"""
|
|
||||||
self.update_downloads_completed(0)
|
|
||||||
self.update_downloads_in_progress(0)
|
|
||||||
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
|
|
||||||
self.downloads.no_downloads_label.show()
|
|
||||||
self.downloads.downloads_container.resize(self.downloads.downloads_container.sizeHint())
|
|
||||||
|
|
||||||
def update_downloads_completed(self, count):
|
|
||||||
"""
|
|
||||||
Update the 'Downloads completed' info widget.
|
|
||||||
"""
|
|
||||||
if count == 0:
|
|
||||||
self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed_none.png')
|
|
||||||
else:
|
|
||||||
self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed.png')
|
|
||||||
self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_completed_downloads_image, count))
|
|
||||||
self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
|
|
||||||
|
|
||||||
def update_downloads_in_progress(self, count):
|
|
||||||
"""
|
|
||||||
Update the 'Downloads in progress' info widget.
|
|
||||||
"""
|
|
||||||
if count == 0:
|
|
||||||
self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_in_progress_none.png')
|
|
||||||
else:
|
|
||||||
self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_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(self.info_in_progress_downloads_image, count))
|
|
||||||
self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
|
|
||||||
|
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
self.common.log('OnionShareGui', 'closeEvent')
|
self.common.log('OnionShareGui', 'closeEvent')
|
||||||
try:
|
try:
|
||||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
if self.mode == OnionShareGui.MODE_SHARE:
|
||||||
|
server_status = self.share_mode.server_status
|
||||||
|
else:
|
||||||
|
server_status = self.receive_mode.server_status
|
||||||
|
if server_status.status != server_status.STATUS_STOPPED:
|
||||||
self.common.log('OnionShareGui', 'closeEvent, opening warning dialog')
|
self.common.log('OnionShareGui', 'closeEvent, opening warning dialog')
|
||||||
dialog = QtWidgets.QMessageBox()
|
dialog = QtWidgets.QMessageBox()
|
||||||
dialog.setWindowTitle(strings._('gui_quit_title', True))
|
dialog.setWindowTitle(strings._('gui_quit_title', True))
|
||||||
dialog.setText(strings._('gui_quit_warning', True))
|
if self.mode == OnionShareGui.MODE_SHARE:
|
||||||
|
dialog.setText(strings._('gui_share_quit_warning', True))
|
||||||
|
else:
|
||||||
|
dialog.setText(strings._('gui_receive_quit_warning', True))
|
||||||
dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
||||||
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
|
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
|
||||||
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
|
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
|
||||||
@ -752,84 +477,3 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
except:
|
except:
|
||||||
e.accept()
|
e.accept()
|
||||||
|
|
||||||
|
|
||||||
class ZipProgressBar(QtWidgets.QProgressBar):
|
|
||||||
update_processed_size_signal = QtCore.pyqtSignal(int)
|
|
||||||
|
|
||||||
def __init__(self, total_files_size):
|
|
||||||
super(ZipProgressBar, self).__init__()
|
|
||||||
self.setMaximumHeight(20)
|
|
||||||
self.setMinimumWidth(200)
|
|
||||||
self.setValue(0)
|
|
||||||
self.setFormat(strings._('zip_progress_bar_format'))
|
|
||||||
cssStyleData ="""
|
|
||||||
QProgressBar {
|
|
||||||
border: 1px solid #4e064f;
|
|
||||||
background-color: #ffffff !important;
|
|
||||||
text-align: center;
|
|
||||||
color: #9b9b9b;
|
|
||||||
}
|
|
||||||
|
|
||||||
QProgressBar::chunk {
|
|
||||||
border: 0px;
|
|
||||||
background-color: #4e064f;
|
|
||||||
width: 10px;
|
|
||||||
}"""
|
|
||||||
self.setStyleSheet(cssStyleData)
|
|
||||||
|
|
||||||
self._total_files_size = total_files_size
|
|
||||||
self._processed_size = 0
|
|
||||||
|
|
||||||
self.update_processed_size_signal.connect(self.update_processed_size)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_files_size(self):
|
|
||||||
return self._total_files_size
|
|
||||||
|
|
||||||
@total_files_size.setter
|
|
||||||
def total_files_size(self, val):
|
|
||||||
self._total_files_size = val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processed_size(self):
|
|
||||||
return self._processed_size
|
|
||||||
|
|
||||||
@processed_size.setter
|
|
||||||
def processed_size(self, val):
|
|
||||||
self.update_processed_size(val)
|
|
||||||
|
|
||||||
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:
|
|
||||||
self.setValue(100)
|
|
||||||
else:
|
|
||||||
self.setValue(0)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
231
onionshare_gui/receive_mode/__init__.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# -*- 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, QtWidgets, QtGui
|
||||||
|
|
||||||
|
from onionshare import strings
|
||||||
|
from onionshare.web import Web
|
||||||
|
|
||||||
|
from .uploads import Uploads
|
||||||
|
from ..mode import Mode
|
||||||
|
|
||||||
|
class ReceiveMode(Mode):
|
||||||
|
"""
|
||||||
|
Parts of the main window UI for receiving files.
|
||||||
|
"""
|
||||||
|
def init(self):
|
||||||
|
"""
|
||||||
|
Custom initialization for ReceiveMode.
|
||||||
|
"""
|
||||||
|
# Create the Web object
|
||||||
|
self.web = Web(self.common, True, True)
|
||||||
|
|
||||||
|
# Server status
|
||||||
|
self.server_status.set_mode('receive')
|
||||||
|
self.server_status.server_started_finished.connect(self.update_primary_action)
|
||||||
|
self.server_status.server_stopped.connect(self.update_primary_action)
|
||||||
|
self.server_status.server_canceled.connect(self.update_primary_action)
|
||||||
|
|
||||||
|
# Tell server_status about web, then update
|
||||||
|
self.server_status.web = self.web
|
||||||
|
self.server_status.update()
|
||||||
|
|
||||||
|
# Downloads
|
||||||
|
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'])
|
||||||
|
|
||||||
|
self.info_completed_uploads_count = QtWidgets.QLabel()
|
||||||
|
self.info_completed_uploads_count.setStyleSheet(self.common.css['mode_info_label'])
|
||||||
|
|
||||||
|
self.update_uploads_completed()
|
||||||
|
self.update_uploads_in_progress()
|
||||||
|
|
||||||
|
self.info_layout = QtWidgets.QHBoxLayout()
|
||||||
|
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)
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
# Receive mode info
|
||||||
|
self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True))
|
||||||
|
self.receive_info.setMinimumHeight(80)
|
||||||
|
self.receive_info.setWordWrap(True)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
self.layout.insertWidget(0, self.receive_info)
|
||||||
|
self.layout.insertWidget(0, self.info_widget)
|
||||||
|
|
||||||
|
def get_stop_server_shutdown_timeout_text(self):
|
||||||
|
"""
|
||||||
|
Return the string to put on the stop server button, if there's a shutdown timeout
|
||||||
|
"""
|
||||||
|
return strings._('gui_receive_stop_server_shutdown_timeout', True)
|
||||||
|
|
||||||
|
def timeout_finished_should_stop_server(self):
|
||||||
|
"""
|
||||||
|
The shutdown timer expired, should we stop the server? Returns a bool
|
||||||
|
"""
|
||||||
|
# TODO: wait until the final upload is done before stoppign the server?
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start_server_custom(self):
|
||||||
|
"""
|
||||||
|
Starting the server.
|
||||||
|
"""
|
||||||
|
# Reset web counters
|
||||||
|
self.web.upload_count = 0
|
||||||
|
self.web.error404_count = 0
|
||||||
|
|
||||||
|
# Hide and reset the uploads if we have previously shared
|
||||||
|
self.reset_info_counters()
|
||||||
|
|
||||||
|
def start_server_step2_custom(self):
|
||||||
|
"""
|
||||||
|
Step 2 in starting the server.
|
||||||
|
"""
|
||||||
|
# Continue
|
||||||
|
self.starting_server_step3.emit()
|
||||||
|
self.start_server_finished.emit()
|
||||||
|
|
||||||
|
def handle_tor_broke_custom(self):
|
||||||
|
"""
|
||||||
|
Connection to Tor broke.
|
||||||
|
"""
|
||||||
|
self.primary_action.hide()
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
def handle_request_load(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_LOAD event.
|
||||||
|
"""
|
||||||
|
self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True))
|
||||||
|
|
||||||
|
def handle_request_started(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_STARTED event.
|
||||||
|
"""
|
||||||
|
self.uploads.add(event["data"]["id"], event["data"]["content_length"])
|
||||||
|
self.uploads_in_progress += 1
|
||||||
|
self.update_uploads_in_progress()
|
||||||
|
|
||||||
|
self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True))
|
||||||
|
|
||||||
|
def handle_request_progress(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_PROGRESS event.
|
||||||
|
"""
|
||||||
|
self.uploads.update(event["data"]["id"], event["data"]["progress"])
|
||||||
|
|
||||||
|
def handle_request_close_server(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_CLOSE_SERVER event.
|
||||||
|
"""
|
||||||
|
self.stop_server()
|
||||||
|
self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True))
|
||||||
|
|
||||||
|
def handle_request_upload_file_renamed(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_UPLOAD_FILE_RENAMED event.
|
||||||
|
"""
|
||||||
|
self.uploads.rename(event["data"]["id"], event["data"]["old_filename"], event["data"]["new_filename"])
|
||||||
|
|
||||||
|
def handle_request_upload_finished(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_UPLOAD_FINISHED event.
|
||||||
|
"""
|
||||||
|
self.uploads.finished(event["data"]["id"])
|
||||||
|
|
||||||
|
def on_reload_settings(self):
|
||||||
|
"""
|
||||||
|
We should be ok to re-enable the 'Start Receive Mode' button now.
|
||||||
|
"""
|
||||||
|
self.primary_action.show()
|
||||||
|
self.info_widget.show()
|
||||||
|
|
||||||
|
def reset_info_counters(self):
|
||||||
|
"""
|
||||||
|
Set the info counters back to zero.
|
||||||
|
"""
|
||||||
|
self.uploads_completed = 0
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if self.uploads_completed == 0:
|
||||||
|
image = self.common.get_resource_path('images/share_completed_none.png')
|
||||||
|
else:
|
||||||
|
image = self.common.get_resource_path('images/share_completed.png')
|
||||||
|
self.info_completed_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_completed))
|
||||||
|
self.info_completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.uploads_completed))
|
||||||
|
|
||||||
|
def update_uploads_in_progress(self):
|
||||||
|
"""
|
||||||
|
Update the 'Downloads 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))
|
||||||
|
|
||||||
|
def update_primary_action(self):
|
||||||
|
self.common.log('ReceiveMode', 'update_primary_action')
|
||||||
|
|
||||||
|
# Show the info widget when the server is active
|
||||||
|
if self.server_status.status == self.server_status.STATUS_STARTED:
|
||||||
|
self.info_widget.show()
|
||||||
|
else:
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
# 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()
|
310
onionshare_gui/receive_mode/uploads.py
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
# -*- 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 os
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
|
from onionshare import strings
|
||||||
|
from ..widgets import Alert
|
||||||
|
|
||||||
|
|
||||||
|
class File(QtWidgets.QWidget):
|
||||||
|
def __init__(self, common, filename):
|
||||||
|
super(File, self).__init__()
|
||||||
|
self.common = common
|
||||||
|
|
||||||
|
self.common.log('File', '__init__', 'filename: {}'.format(filename))
|
||||||
|
|
||||||
|
self.filename = filename
|
||||||
|
self.started = datetime.now()
|
||||||
|
|
||||||
|
# Filename label
|
||||||
|
self.filename_label = QtWidgets.QLabel(self.filename)
|
||||||
|
self.filename_label_width = self.filename_label.width()
|
||||||
|
|
||||||
|
# File size label
|
||||||
|
self.filesize_label = QtWidgets.QLabel()
|
||||||
|
self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
|
||||||
|
self.filesize_label.hide()
|
||||||
|
|
||||||
|
# Folder button
|
||||||
|
folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
|
||||||
|
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)
|
||||||
|
self.folder_button.hide()
|
||||||
|
|
||||||
|
# Layouts
|
||||||
|
layout = QtWidgets.QHBoxLayout()
|
||||||
|
layout.addWidget(self.filename_label)
|
||||||
|
layout.addWidget(self.filesize_label)
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addWidget(self.folder_button)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def update(self, uploaded_bytes, complete):
|
||||||
|
self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes))
|
||||||
|
self.filesize_label.show()
|
||||||
|
|
||||||
|
if complete:
|
||||||
|
self.folder_button.show()
|
||||||
|
|
||||||
|
def rename(self, new_filename):
|
||||||
|
self.filename = new_filename
|
||||||
|
self.filename_label.setText(self.filename)
|
||||||
|
|
||||||
|
def open_folder(self):
|
||||||
|
"""
|
||||||
|
Open the downloads folder, with the file selected, in a cross-platform manner
|
||||||
|
"""
|
||||||
|
self.common.log('File', 'open_folder')
|
||||||
|
|
||||||
|
abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename)
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
|
||||||
|
try:
|
||||||
|
# If nautilus is available, open it
|
||||||
|
subprocess.Popen(['nautilus', abs_filename])
|
||||||
|
except:
|
||||||
|
Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
elif self.common.platform == 'Darwin':
|
||||||
|
# TODO: Implement opening folder with file selected in macOS
|
||||||
|
# This seems helpful: https://stackoverflow.com/questions/3520493/python-show-in-finder
|
||||||
|
self.common.log('File', 'open_folder', 'not implemented for Darwin yet')
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
elif self.common.platform == 'Windows':
|
||||||
|
# TODO: Implement opening folder with file selected in Windows
|
||||||
|
# This seems helpful: https://stackoverflow.com/questions/6631299/python-opening-a-folder-in-explorer-nautilus-mac-thingie
|
||||||
|
self.common.log('File', 'open_folder', 'not implemented for Windows yet')
|
||||||
|
|
||||||
|
|
||||||
|
class Upload(QtWidgets.QWidget):
|
||||||
|
def __init__(self, common, upload_id, content_length):
|
||||||
|
super(Upload, self).__init__()
|
||||||
|
self.common = common
|
||||||
|
self.upload_id = upload_id
|
||||||
|
self.content_length = content_length
|
||||||
|
self.started = datetime.now()
|
||||||
|
|
||||||
|
# Label
|
||||||
|
self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p")))
|
||||||
|
|
||||||
|
# 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.setValue(0)
|
||||||
|
self.progress_bar.setStyleSheet(self.common.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.css['receive_file'])
|
||||||
|
files_widget.setLayout(self.files_layout)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
layout.addWidget(files_widget)
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# We're also making a dictionary of file widgets, to make them easier to access
|
||||||
|
self.files = {}
|
||||||
|
|
||||||
|
def update(self, progress):
|
||||||
|
"""
|
||||||
|
Using the progress from Web, update the progress bar and file size labels
|
||||||
|
for each file
|
||||||
|
"""
|
||||||
|
total_uploaded_bytes = 0
|
||||||
|
for filename in progress:
|
||||||
|
total_uploaded_bytes += progress[filename]['uploaded_bytes']
|
||||||
|
|
||||||
|
# Update the progress bar
|
||||||
|
self.progress_bar.setMaximum(self.content_length)
|
||||||
|
self.progress_bar.setValue(total_uploaded_bytes)
|
||||||
|
|
||||||
|
elapsed = datetime.now() - self.started
|
||||||
|
if elapsed.seconds < 10:
|
||||||
|
pb_fmt = strings._('gui_download_upload_progress_starting').format(
|
||||||
|
self.common.human_readable_filesize(total_uploaded_bytes))
|
||||||
|
else:
|
||||||
|
estimated_time_remaining = self.common.estimated_time_remaining(
|
||||||
|
total_uploaded_bytes,
|
||||||
|
self.content_length,
|
||||||
|
self.started.timestamp())
|
||||||
|
pb_fmt = strings._('gui_download_upload_progress_eta').format(
|
||||||
|
self.common.human_readable_filesize(total_uploaded_bytes),
|
||||||
|
estimated_time_remaining)
|
||||||
|
|
||||||
|
# Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
|
||||||
|
for filename in list(progress):
|
||||||
|
# Add a new file if needed
|
||||||
|
if filename not in self.files:
|
||||||
|
self.files[filename] = File(self.common, filename)
|
||||||
|
self.files_layout.addWidget(self.files[filename])
|
||||||
|
|
||||||
|
# Update the file
|
||||||
|
self.files[filename].update(progress[filename]['uploaded_bytes'], progress[filename]['complete'])
|
||||||
|
|
||||||
|
def rename(self, old_filename, new_filename):
|
||||||
|
self.files[old_filename].rename(new_filename)
|
||||||
|
self.files[new_filename] = self.files.pop(old_filename)
|
||||||
|
|
||||||
|
def finished(self):
|
||||||
|
# Hide the progress bar
|
||||||
|
self.progress_bar.hide()
|
||||||
|
|
||||||
|
# Change the label
|
||||||
|
self.ended = self.started = datetime.now()
|
||||||
|
if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day:
|
||||||
|
if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute:
|
||||||
|
text = strings._('gui_upload_finished', True).format(
|
||||||
|
self.started.strftime("%b %d, %I:%M%p")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = strings._('gui_upload_finished_range', True).format(
|
||||||
|
self.started.strftime("%b %d, %I:%M%p"),
|
||||||
|
self.ended.strftime("%I:%M%p")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = strings._('gui_upload_finished_range', True).format(
|
||||||
|
self.started.strftime("%b %d, %I:%M%p"),
|
||||||
|
self.ended.strftime("%b %d, %I:%M%p")
|
||||||
|
)
|
||||||
|
self.label.setText(text)
|
||||||
|
|
||||||
|
|
||||||
|
class Uploads(QtWidgets.QScrollArea):
|
||||||
|
"""
|
||||||
|
The uploads chunk of the GUI. This lists all of the active upload
|
||||||
|
progress bars, as well as information about each upload.
|
||||||
|
"""
|
||||||
|
def __init__(self, common):
|
||||||
|
super(Uploads, self).__init__()
|
||||||
|
self.common = common
|
||||||
|
self.common.log('Uploads', '__init__')
|
||||||
|
|
||||||
|
self.resizeEvent = None
|
||||||
|
|
||||||
|
self.uploads = {}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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.uploads_layout = QtWidgets.QVBoxLayout()
|
||||||
|
|
||||||
|
widget = QtWidgets.QWidget()
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
layout.addWidget(uploads_label)
|
||||||
|
layout.addWidget(self.no_uploads_label)
|
||||||
|
layout.addLayout(self.uploads_layout)
|
||||||
|
layout.addStretch()
|
||||||
|
widget.setLayout(layout)
|
||||||
|
self.setWidget(widget)
|
||||||
|
|
||||||
|
def add(self, upload_id, content_length):
|
||||||
|
"""
|
||||||
|
Add a new upload.
|
||||||
|
"""
|
||||||
|
self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length))
|
||||||
|
# Hide the no_uploads_label
|
||||||
|
self.no_uploads_label.hide()
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
self.uploads[upload_id].update(progress)
|
||||||
|
|
||||||
|
def rename(self, upload_id, old_filename, new_filename):
|
||||||
|
"""
|
||||||
|
Rename a file, which happens if the filename already exists in downloads_dir.
|
||||||
|
"""
|
||||||
|
self.uploads[upload_id].rename(old_filename, new_filename)
|
||||||
|
|
||||||
|
def finished(self, upload_id):
|
||||||
|
"""
|
||||||
|
An upload has finished.
|
||||||
|
"""
|
||||||
|
self.uploads[upload_id].finished()
|
||||||
|
|
||||||
|
def cancel(self, upload_id):
|
||||||
|
"""
|
||||||
|
Update an upload progress bar to show that it has been canceled.
|
||||||
|
"""
|
||||||
|
self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id))
|
||||||
|
self.uploads[upload_id].cancel()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Reset the uploads back to zero
|
||||||
|
"""
|
||||||
|
self.common.log('Uploads', 'reset')
|
||||||
|
for upload in self.uploads.values():
|
||||||
|
self.uploads_layout.removeWidget(upload)
|
||||||
|
self.uploads = {}
|
||||||
|
|
||||||
|
self.no_uploads_label.show()
|
||||||
|
self.resize(self.sizeHint())
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
width = self.frameGeometry().width()
|
||||||
|
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)
|
||||||
|
except:
|
||||||
|
pass
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -18,37 +18,46 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
import platform
|
import platform
|
||||||
from .alert import Alert
|
import textwrap
|
||||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
from onionshare import strings
|
from onionshare import strings
|
||||||
|
|
||||||
|
from .widgets import Alert
|
||||||
|
|
||||||
class ServerStatus(QtWidgets.QWidget):
|
class ServerStatus(QtWidgets.QWidget):
|
||||||
"""
|
"""
|
||||||
The server status chunk of the GUI.
|
The server status chunk of the GUI.
|
||||||
"""
|
"""
|
||||||
server_started = QtCore.pyqtSignal()
|
server_started = QtCore.pyqtSignal()
|
||||||
|
server_started_finished = QtCore.pyqtSignal()
|
||||||
server_stopped = QtCore.pyqtSignal()
|
server_stopped = QtCore.pyqtSignal()
|
||||||
server_canceled = QtCore.pyqtSignal()
|
server_canceled = QtCore.pyqtSignal()
|
||||||
button_clicked = QtCore.pyqtSignal()
|
button_clicked = QtCore.pyqtSignal()
|
||||||
url_copied = QtCore.pyqtSignal()
|
url_copied = QtCore.pyqtSignal()
|
||||||
hidservauth_copied = QtCore.pyqtSignal()
|
hidservauth_copied = QtCore.pyqtSignal()
|
||||||
|
|
||||||
|
MODE_SHARE = 'share'
|
||||||
|
MODE_RECEIVE = 'receive'
|
||||||
|
|
||||||
STATUS_STOPPED = 0
|
STATUS_STOPPED = 0
|
||||||
STATUS_WORKING = 1
|
STATUS_WORKING = 1
|
||||||
STATUS_STARTED = 2
|
STATUS_STARTED = 2
|
||||||
|
|
||||||
def __init__(self, common, qtapp, app, web, file_selection):
|
def __init__(self, common, qtapp, app, file_selection=None):
|
||||||
super(ServerStatus, self).__init__()
|
super(ServerStatus, self).__init__()
|
||||||
|
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
self.status = self.STATUS_STOPPED
|
self.status = self.STATUS_STOPPED
|
||||||
|
self.mode = None # Gets set in self.set_mode
|
||||||
|
|
||||||
self.qtapp = qtapp
|
self.qtapp = qtapp
|
||||||
self.app = app
|
self.app = app
|
||||||
self.web = web
|
|
||||||
self.file_selection = file_selection
|
self.web = None
|
||||||
|
|
||||||
|
self.resizeEvent(None)
|
||||||
|
|
||||||
# Shutdown timeout layout
|
# Shutdown timeout layout
|
||||||
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
|
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
|
||||||
@ -70,31 +79,29 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout)
|
self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout)
|
||||||
self.shutdown_timeout_container.hide()
|
self.shutdown_timeout_container.hide()
|
||||||
|
|
||||||
|
|
||||||
# Server layout
|
# Server layout
|
||||||
self.server_button = QtWidgets.QPushButton()
|
self.server_button = QtWidgets.QPushButton()
|
||||||
self.server_button.clicked.connect(self.server_button_clicked)
|
self.server_button.clicked.connect(self.server_button_clicked)
|
||||||
|
|
||||||
# URL layout
|
# URL layout
|
||||||
url_font = QtGui.QFont()
|
url_font = QtGui.QFont()
|
||||||
self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True))
|
self.url_description = QtWidgets.QLabel()
|
||||||
self.url_description.setWordWrap(True)
|
self.url_description.setWordWrap(True)
|
||||||
self.url_description.setMinimumHeight(50)
|
self.url_description.setMinimumHeight(50)
|
||||||
self.url = QtWidgets.QLabel()
|
self.url = QtWidgets.QLabel()
|
||||||
self.url.setFont(url_font)
|
self.url.setFont(url_font)
|
||||||
self.url.setWordWrap(True)
|
self.url.setWordWrap(True)
|
||||||
self.url.setMinimumHeight(60)
|
self.url.setMinimumHeight(65)
|
||||||
self.url.setMinimumSize(self.url.sizeHint())
|
self.url.setMinimumSize(self.url.sizeHint())
|
||||||
self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }')
|
self.url.setStyleSheet(self.common.css['server_status_url'])
|
||||||
|
|
||||||
url_buttons_style = 'QPushButton { color: #3f7fcf; }'
|
|
||||||
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
|
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
|
||||||
self.copy_url_button.setFlat(True)
|
self.copy_url_button.setFlat(True)
|
||||||
self.copy_url_button.setStyleSheet(url_buttons_style)
|
self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons'])
|
||||||
self.copy_url_button.clicked.connect(self.copy_url)
|
self.copy_url_button.clicked.connect(self.copy_url)
|
||||||
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
|
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
|
||||||
self.copy_hidservauth_button.setFlat(True)
|
self.copy_hidservauth_button.setFlat(True)
|
||||||
self.copy_hidservauth_button.setStyleSheet(url_buttons_style)
|
self.copy_hidservauth_button.setStyleSheet(self.common.css['server_status_url_buttons'])
|
||||||
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
|
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
|
||||||
url_buttons_layout = QtWidgets.QHBoxLayout()
|
url_buttons_layout = QtWidgets.QHBoxLayout()
|
||||||
url_buttons_layout.addWidget(self.copy_url_button)
|
url_buttons_layout.addWidget(self.copy_url_button)
|
||||||
@ -113,8 +120,35 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
layout.addWidget(self.shutdown_timeout_container)
|
layout.addWidget(self.shutdown_timeout_container)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def set_mode(self, share_mode, file_selection=None):
|
||||||
|
"""
|
||||||
|
The server status is in share mode.
|
||||||
|
"""
|
||||||
|
self.mode = share_mode
|
||||||
|
|
||||||
|
if self.mode == ServerStatus.MODE_SHARE:
|
||||||
|
self.file_selection = file_selection
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
"""
|
||||||
|
When the widget is resized, try and adjust the display of a v3 onion URL.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.get_url()
|
||||||
|
url_length=len(self.get_url())
|
||||||
|
if url_length > 60:
|
||||||
|
width = self.frameGeometry().width()
|
||||||
|
if width < 530:
|
||||||
|
wrapped_onion_url = textwrap.fill(self.get_url(), 50)
|
||||||
|
self.url.setText(wrapped_onion_url)
|
||||||
|
else:
|
||||||
|
self.url.setText(self.get_url())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def shutdown_timeout_reset(self):
|
def shutdown_timeout_reset(self):
|
||||||
"""
|
"""
|
||||||
Reset the timeout in the UI after stopping a share
|
Reset the timeout in the UI after stopping a share
|
||||||
@ -131,20 +165,25 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
self.url_description.show()
|
self.url_description.show()
|
||||||
|
|
||||||
info_image = self.common.get_resource_path('images/info.png')
|
info_image = self.common.get_resource_path('images/info.png')
|
||||||
self.url_description.setText(strings._('gui_url_description', True).format(info_image))
|
|
||||||
|
if self.mode == ServerStatus.MODE_SHARE:
|
||||||
|
self.url_description.setText(strings._('gui_share_url_description', True).format(info_image))
|
||||||
|
else:
|
||||||
|
self.url_description.setText(strings._('gui_receive_url_description', True).format(info_image))
|
||||||
|
|
||||||
# Show a Tool Tip explaining the lifecycle of this URL
|
# Show a Tool Tip explaining the lifecycle of this URL
|
||||||
if self.common.settings.get('save_private_key'):
|
if self.common.settings.get('save_private_key'):
|
||||||
if self.common.settings.get('close_after_first_download'):
|
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'):
|
||||||
self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
|
self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
|
||||||
else:
|
else:
|
||||||
self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
|
self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
|
||||||
else:
|
else:
|
||||||
if self.common.settings.get('close_after_first_download'):
|
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'):
|
||||||
self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
|
self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
|
||||||
else:
|
else:
|
||||||
self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
|
self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
|
||||||
|
|
||||||
self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
|
self.url.setText(self.get_url())
|
||||||
self.url.show()
|
self.url.show()
|
||||||
|
|
||||||
self.copy_url_button.show()
|
self.copy_url_button.show()
|
||||||
@ -168,36 +207,43 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
self.copy_hidservauth_button.hide()
|
self.copy_hidservauth_button.hide()
|
||||||
|
|
||||||
# Button
|
# Button
|
||||||
button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
|
if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0:
|
||||||
button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }'
|
|
||||||
button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
|
|
||||||
if self.file_selection.get_num_files() == 0:
|
|
||||||
self.server_button.hide()
|
self.server_button.hide()
|
||||||
else:
|
else:
|
||||||
self.server_button.show()
|
self.server_button.show()
|
||||||
|
|
||||||
if self.status == self.STATUS_STOPPED:
|
if self.status == self.STATUS_STOPPED:
|
||||||
self.server_button.setStyleSheet(button_stopped_style)
|
self.server_button.setStyleSheet(self.common.css['server_status_button_stopped'])
|
||||||
self.server_button.setEnabled(True)
|
self.server_button.setEnabled(True)
|
||||||
self.server_button.setText(strings._('gui_start_server', True))
|
if self.mode == ServerStatus.MODE_SHARE:
|
||||||
|
self.server_button.setText(strings._('gui_share_start_server', True))
|
||||||
|
else:
|
||||||
|
self.server_button.setText(strings._('gui_receive_start_server', True))
|
||||||
self.server_button.setToolTip('')
|
self.server_button.setToolTip('')
|
||||||
if self.common.settings.get('shutdown_timeout'):
|
if self.common.settings.get('shutdown_timeout'):
|
||||||
self.shutdown_timeout_container.show()
|
self.shutdown_timeout_container.show()
|
||||||
elif self.status == self.STATUS_STARTED:
|
elif self.status == self.STATUS_STARTED:
|
||||||
self.server_button.setStyleSheet(button_started_style)
|
self.server_button.setStyleSheet(self.common.css['server_status_button_started'])
|
||||||
self.server_button.setEnabled(True)
|
self.server_button.setEnabled(True)
|
||||||
self.server_button.setText(strings._('gui_stop_server', True))
|
if self.mode == ServerStatus.MODE_SHARE:
|
||||||
|
self.server_button.setText(strings._('gui_share_stop_server', True))
|
||||||
|
else:
|
||||||
|
self.server_button.setText(strings._('gui_receive_stop_server', True))
|
||||||
if self.common.settings.get('shutdown_timeout'):
|
if self.common.settings.get('shutdown_timeout'):
|
||||||
self.shutdown_timeout_container.hide()
|
self.shutdown_timeout_container.hide()
|
||||||
self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
|
if self.mode == ServerStatus.MODE_SHARE:
|
||||||
|
self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
|
||||||
|
else:
|
||||||
|
self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
|
||||||
|
|
||||||
elif self.status == self.STATUS_WORKING:
|
elif self.status == self.STATUS_WORKING:
|
||||||
self.server_button.setStyleSheet(button_working_style)
|
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
|
||||||
self.server_button.setEnabled(True)
|
self.server_button.setEnabled(True)
|
||||||
self.server_button.setText(strings._('gui_please_wait'))
|
self.server_button.setText(strings._('gui_please_wait'))
|
||||||
if self.common.settings.get('shutdown_timeout'):
|
if self.common.settings.get('shutdown_timeout'):
|
||||||
self.shutdown_timeout_container.hide()
|
self.shutdown_timeout_container.hide()
|
||||||
else:
|
else:
|
||||||
self.server_button.setStyleSheet(button_working_style)
|
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
|
||||||
self.server_button.setEnabled(False)
|
self.server_button.setEnabled(False)
|
||||||
self.server_button.setText(strings._('gui_please_wait'))
|
self.server_button.setText(strings._('gui_please_wait'))
|
||||||
if self.common.settings.get('shutdown_timeout'):
|
if self.common.settings.get('shutdown_timeout'):
|
||||||
@ -239,6 +285,7 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
self.status = self.STATUS_STARTED
|
self.status = self.STATUS_STARTED
|
||||||
self.copy_url()
|
self.copy_url()
|
||||||
self.update()
|
self.update()
|
||||||
|
self.server_started_finished.emit()
|
||||||
|
|
||||||
def stop_server(self):
|
def stop_server(self):
|
||||||
"""
|
"""
|
||||||
@ -270,10 +317,8 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
"""
|
"""
|
||||||
Copy the onionshare URL to the clipboard.
|
Copy the onionshare URL to the clipboard.
|
||||||
"""
|
"""
|
||||||
url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)
|
|
||||||
|
|
||||||
clipboard = self.qtapp.clipboard()
|
clipboard = self.qtapp.clipboard()
|
||||||
clipboard.setText(url)
|
clipboard.setText(self.get_url())
|
||||||
|
|
||||||
self.url_copied.emit()
|
self.url_copied.emit()
|
||||||
|
|
||||||
@ -285,3 +330,13 @@ class ServerStatus(QtWidgets.QWidget):
|
|||||||
clipboard.setText(self.app.auth_string)
|
clipboard.setText(self.app.auth_string)
|
||||||
|
|
||||||
self.hidservauth_copied.emit()
|
self.hidservauth_copied.emit()
|
||||||
|
|
||||||
|
def get_url(self):
|
||||||
|
"""
|
||||||
|
Returns the OnionShare URL.
|
||||||
|
"""
|
||||||
|
if self.common.settings.get('public_mode'):
|
||||||
|
url = 'http://{0:s}'.format(self.app.onion_host)
|
||||||
|
else:
|
||||||
|
url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)
|
||||||
|
return url
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -24,7 +24,7 @@ from onionshare import strings, common
|
|||||||
from onionshare.settings import Settings
|
from onionshare.settings import Settings
|
||||||
from onionshare.onion import *
|
from onionshare.onion import *
|
||||||
|
|
||||||
from .alert import Alert
|
from .widgets import Alert
|
||||||
from .update_checker import *
|
from .update_checker import *
|
||||||
from .tor_connection_dialog import TorConnectionDialog
|
from .tor_connection_dialog import TorConnectionDialog
|
||||||
|
|
||||||
@ -52,48 +52,93 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
|
|
||||||
self.system = platform.system()
|
self.system = platform.system()
|
||||||
|
|
||||||
# Sharing options
|
# General options
|
||||||
|
|
||||||
# Close after first download
|
# Use a slug or not ('public mode')
|
||||||
self.close_after_first_download_checkbox = QtWidgets.QCheckBox()
|
self.public_mode_checkbox = QtWidgets.QCheckBox()
|
||||||
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
|
self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option", True))
|
self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox", True))
|
||||||
|
public_mode_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Public-Mode"))
|
||||||
|
public_mode_label.setStyleSheet(self.common.css['settings_whats_this'])
|
||||||
|
public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||||
|
public_mode_label.setOpenExternalLinks(True)
|
||||||
|
public_mode_label.setMinimumSize(public_mode_label.sizeHint())
|
||||||
|
public_mode_layout = QtWidgets.QHBoxLayout()
|
||||||
|
public_mode_layout.addWidget(self.public_mode_checkbox)
|
||||||
|
public_mode_layout.addWidget(public_mode_label)
|
||||||
|
public_mode_layout.addStretch()
|
||||||
|
public_mode_layout.setContentsMargins(0,0,0,0)
|
||||||
|
self.public_mode_widget = QtWidgets.QWidget()
|
||||||
|
self.public_mode_widget.setLayout(public_mode_layout)
|
||||||
|
|
||||||
# Whether or not to show systray notifications
|
# Whether or not to use a shutdown ('auto-stop') timer
|
||||||
self.systray_notifications_checkbox = QtWidgets.QCheckBox()
|
|
||||||
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
|
|
||||||
self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True))
|
|
||||||
|
|
||||||
# Whether or not to use a shutdown timer
|
|
||||||
self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
|
self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
|
||||||
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
|
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
|
self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
|
||||||
|
shutdown_timeout_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer"))
|
||||||
|
shutdown_timeout_label.setStyleSheet(self.common.css['settings_whats_this'])
|
||||||
|
shutdown_timeout_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||||
|
shutdown_timeout_label.setOpenExternalLinks(True)
|
||||||
|
shutdown_timeout_label.setMinimumSize(public_mode_label.sizeHint())
|
||||||
|
shutdown_timeout_layout = QtWidgets.QHBoxLayout()
|
||||||
|
shutdown_timeout_layout.addWidget(self.shutdown_timeout_checkbox)
|
||||||
|
shutdown_timeout_layout.addWidget(shutdown_timeout_label)
|
||||||
|
shutdown_timeout_layout.addStretch()
|
||||||
|
shutdown_timeout_layout.setContentsMargins(0,0,0,0)
|
||||||
|
self.shutdown_timeout_widget = QtWidgets.QWidget()
|
||||||
|
self.shutdown_timeout_widget.setLayout(shutdown_timeout_layout)
|
||||||
|
|
||||||
# Whether or not to save the Onion private key for reuse
|
# Whether or not to use legacy v2 onions
|
||||||
|
self.use_legacy_v2_onions_checkbox = QtWidgets.QCheckBox()
|
||||||
|
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
self.use_legacy_v2_onions_checkbox.setText(strings._("gui_use_legacy_v2_onions_checkbox", True))
|
||||||
|
self.use_legacy_v2_onions_checkbox.clicked.connect(self.use_legacy_v2_onions_checkbox_clicked)
|
||||||
|
use_legacy_v2_onions_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Legacy-Addresses"))
|
||||||
|
use_legacy_v2_onions_label.setStyleSheet(self.common.css['settings_whats_this'])
|
||||||
|
use_legacy_v2_onions_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||||
|
use_legacy_v2_onions_label.setOpenExternalLinks(True)
|
||||||
|
use_legacy_v2_onions_layout = QtWidgets.QHBoxLayout()
|
||||||
|
use_legacy_v2_onions_layout.addWidget(self.use_legacy_v2_onions_checkbox)
|
||||||
|
use_legacy_v2_onions_layout.addWidget(use_legacy_v2_onions_label)
|
||||||
|
use_legacy_v2_onions_layout.addStretch()
|
||||||
|
use_legacy_v2_onions_layout.setContentsMargins(0,0,0,0)
|
||||||
|
self.use_legacy_v2_onions_widget = QtWidgets.QWidget()
|
||||||
|
self.use_legacy_v2_onions_widget.setLayout(use_legacy_v2_onions_layout)
|
||||||
|
|
||||||
|
# Whether or not to save the Onion private key for reuse (persistent URL mode)
|
||||||
self.save_private_key_checkbox = QtWidgets.QCheckBox()
|
self.save_private_key_checkbox = QtWidgets.QCheckBox()
|
||||||
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True))
|
self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True))
|
||||||
|
self.save_private_key_checkbox.clicked.connect(self.save_private_key_checkbox_clicked)
|
||||||
# Sharing options layout
|
save_private_key_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL"))
|
||||||
sharing_group_layout = QtWidgets.QVBoxLayout()
|
save_private_key_label.setStyleSheet(self.common.css['settings_whats_this'])
|
||||||
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
|
save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||||
sharing_group_layout.addWidget(self.systray_notifications_checkbox)
|
save_private_key_label.setOpenExternalLinks(True)
|
||||||
sharing_group_layout.addWidget(self.shutdown_timeout_checkbox)
|
save_private_key_layout = QtWidgets.QHBoxLayout()
|
||||||
sharing_group_layout.addWidget(self.save_private_key_checkbox)
|
save_private_key_layout.addWidget(self.save_private_key_checkbox)
|
||||||
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
|
save_private_key_layout.addWidget(save_private_key_label)
|
||||||
sharing_group.setLayout(sharing_group_layout)
|
save_private_key_layout.addStretch()
|
||||||
|
save_private_key_layout.setContentsMargins(0,0,0,0)
|
||||||
# Stealth options
|
self.save_private_key_widget = QtWidgets.QWidget()
|
||||||
|
self.save_private_key_widget.setLayout(save_private_key_layout)
|
||||||
|
|
||||||
# Stealth
|
# Stealth
|
||||||
stealth_details = QtWidgets.QLabel(strings._("gui_settings_stealth_option_details", True))
|
|
||||||
stealth_details.setWordWrap(True)
|
|
||||||
stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
|
||||||
stealth_details.setOpenExternalLinks(True)
|
|
||||||
stealth_details.setMinimumSize(stealth_details.sizeHint())
|
|
||||||
self.stealth_checkbox = QtWidgets.QCheckBox()
|
self.stealth_checkbox = QtWidgets.QCheckBox()
|
||||||
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
|
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
|
||||||
|
self.stealth_checkbox.clicked.connect(self.stealth_checkbox_clicked_connect)
|
||||||
|
use_stealth_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services"))
|
||||||
|
use_stealth_label.setStyleSheet(self.common.css['settings_whats_this'])
|
||||||
|
use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||||
|
use_stealth_label.setOpenExternalLinks(True)
|
||||||
|
use_stealth_label.setMinimumSize(use_stealth_label.sizeHint())
|
||||||
|
use_stealth_layout = QtWidgets.QHBoxLayout()
|
||||||
|
use_stealth_layout.addWidget(self.stealth_checkbox)
|
||||||
|
use_stealth_layout.addWidget(use_stealth_label)
|
||||||
|
use_stealth_layout.addStretch()
|
||||||
|
use_stealth_layout.setContentsMargins(0,0,0,0)
|
||||||
|
self.use_stealth_widget = QtWidgets.QWidget()
|
||||||
|
self.use_stealth_widget.setLayout(use_stealth_layout)
|
||||||
|
|
||||||
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
|
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
|
||||||
hidservauth_details.setWordWrap(True)
|
hidservauth_details.setWordWrap(True)
|
||||||
@ -104,14 +149,54 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked)
|
self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked)
|
||||||
self.hidservauth_copy_button.hide()
|
self.hidservauth_copy_button.hide()
|
||||||
|
|
||||||
# Stealth options layout
|
# General options layout
|
||||||
stealth_group_layout = QtWidgets.QVBoxLayout()
|
general_group_layout = QtWidgets.QVBoxLayout()
|
||||||
stealth_group_layout.addWidget(stealth_details)
|
general_group_layout.addWidget(self.public_mode_widget)
|
||||||
stealth_group_layout.addWidget(self.stealth_checkbox)
|
general_group_layout.addWidget(self.shutdown_timeout_widget)
|
||||||
stealth_group_layout.addWidget(hidservauth_details)
|
general_group_layout.addWidget(self.use_legacy_v2_onions_widget)
|
||||||
stealth_group_layout.addWidget(self.hidservauth_copy_button)
|
general_group_layout.addWidget(self.save_private_key_widget)
|
||||||
stealth_group = QtWidgets.QGroupBox(strings._("gui_settings_stealth_label", True))
|
general_group_layout.addWidget(self.use_stealth_widget)
|
||||||
stealth_group.setLayout(stealth_group_layout)
|
general_group_layout.addWidget(hidservauth_details)
|
||||||
|
general_group_layout.addWidget(self.hidservauth_copy_button)
|
||||||
|
|
||||||
|
general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label", True))
|
||||||
|
general_group.setLayout(general_group_layout)
|
||||||
|
|
||||||
|
# Sharing options
|
||||||
|
|
||||||
|
# Close after first download
|
||||||
|
self.close_after_first_download_checkbox = QtWidgets.QCheckBox()
|
||||||
|
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option", True))
|
||||||
|
|
||||||
|
# Sharing options layout
|
||||||
|
sharing_group_layout = QtWidgets.QVBoxLayout()
|
||||||
|
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
|
||||||
|
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
|
||||||
|
sharing_group.setLayout(sharing_group_layout)
|
||||||
|
|
||||||
|
# Downloads dir
|
||||||
|
downloads_label = QtWidgets.QLabel(strings._('gui_settings_downloads_label', True));
|
||||||
|
self.downloads_dir_lineedit = QtWidgets.QLineEdit()
|
||||||
|
self.downloads_dir_lineedit.setReadOnly(True)
|
||||||
|
downloads_button = QtWidgets.QPushButton(strings._('gui_settings_downloads_button', True))
|
||||||
|
downloads_button.clicked.connect(self.downloads_button_clicked)
|
||||||
|
downloads_layout = QtWidgets.QHBoxLayout()
|
||||||
|
downloads_layout.addWidget(downloads_label)
|
||||||
|
downloads_layout.addWidget(self.downloads_dir_lineedit)
|
||||||
|
downloads_layout.addWidget(downloads_button)
|
||||||
|
|
||||||
|
# Allow the receiver to shutdown the server
|
||||||
|
self.receive_allow_receiver_shutdown_checkbox = QtWidgets.QCheckBox()
|
||||||
|
self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
self.receive_allow_receiver_shutdown_checkbox.setText(strings._("gui_settings_receive_allow_receiver_shutdown_checkbox", True))
|
||||||
|
|
||||||
|
# Receiving options layout
|
||||||
|
receiving_group_layout = QtWidgets.QVBoxLayout()
|
||||||
|
receiving_group_layout.addLayout(downloads_layout)
|
||||||
|
receiving_group_layout.addWidget(self.receive_allow_receiver_shutdown_checkbox)
|
||||||
|
receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label", True))
|
||||||
|
receiving_group.setLayout(receiving_group_layout)
|
||||||
|
|
||||||
# Automatic updates options
|
# Automatic updates options
|
||||||
|
|
||||||
@ -325,7 +410,7 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
|
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
|
||||||
self.cancel_button.clicked.connect(self.cancel_clicked)
|
self.cancel_button.clicked.connect(self.cancel_clicked)
|
||||||
version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(self.common.version))
|
version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(self.common.version))
|
||||||
version_label.setStyleSheet('color: #666666')
|
version_label.setStyleSheet(self.common.css['settings_version'])
|
||||||
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
|
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
|
||||||
self.help_button.clicked.connect(self.help_clicked)
|
self.help_button.clicked.connect(self.help_clicked)
|
||||||
buttons_layout = QtWidgets.QHBoxLayout()
|
buttons_layout = QtWidgets.QHBoxLayout()
|
||||||
@ -337,13 +422,14 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
|
|
||||||
# Tor network connection status
|
# Tor network connection status
|
||||||
self.tor_status = QtWidgets.QLabel()
|
self.tor_status = QtWidgets.QLabel()
|
||||||
self.tor_status.setStyleSheet('background-color: #ffffff; color: #000000; padding: 10px')
|
self.tor_status.setStyleSheet(self.common.css['settings_tor_status'])
|
||||||
self.tor_status.hide()
|
self.tor_status.hide()
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
left_col_layout = QtWidgets.QVBoxLayout()
|
left_col_layout = QtWidgets.QVBoxLayout()
|
||||||
|
left_col_layout.addWidget(general_group)
|
||||||
left_col_layout.addWidget(sharing_group)
|
left_col_layout.addWidget(sharing_group)
|
||||||
left_col_layout.addWidget(stealth_group)
|
left_col_layout.addWidget(receiving_group)
|
||||||
left_col_layout.addWidget(autoupdate_group)
|
left_col_layout.addWidget(autoupdate_group)
|
||||||
left_col_layout.addStretch()
|
left_col_layout.addStretch()
|
||||||
|
|
||||||
@ -374,32 +460,60 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
else:
|
else:
|
||||||
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
systray_notifications = self.old_settings.get('systray_notifications')
|
|
||||||
if systray_notifications:
|
|
||||||
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
|
|
||||||
else:
|
|
||||||
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
|
||||||
|
|
||||||
shutdown_timeout = self.old_settings.get('shutdown_timeout')
|
shutdown_timeout = self.old_settings.get('shutdown_timeout')
|
||||||
if shutdown_timeout:
|
if shutdown_timeout:
|
||||||
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
|
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
else:
|
else:
|
||||||
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
|
use_legacy_v2_onions = self.old_settings.get('use_legacy_v2_onions')
|
||||||
|
|
||||||
|
if use_legacy_v2_onions:
|
||||||
|
self.save_private_key_widget.show()
|
||||||
|
self.use_stealth_widget.show()
|
||||||
|
else:
|
||||||
|
self.save_private_key_widget.hide()
|
||||||
|
self.use_stealth_widget.hide()
|
||||||
|
|
||||||
save_private_key = self.old_settings.get('save_private_key')
|
save_private_key = self.old_settings.get('save_private_key')
|
||||||
if save_private_key:
|
if save_private_key:
|
||||||
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
|
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
# Legacy v2 mode is forced on if persistence is enabled
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(True)
|
||||||
|
|
||||||
|
if use_legacy_v2_onions or save_private_key:
|
||||||
|
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
downloads_dir = self.old_settings.get('downloads_dir')
|
||||||
|
self.downloads_dir_lineedit.setText(downloads_dir)
|
||||||
|
|
||||||
|
receive_allow_receiver_shutdown = self.old_settings.get('receive_allow_receiver_shutdown')
|
||||||
|
if receive_allow_receiver_shutdown:
|
||||||
|
self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
else:
|
||||||
|
self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
|
public_mode = self.old_settings.get('public_mode')
|
||||||
|
if public_mode:
|
||||||
|
self.public_mode_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
else:
|
||||||
|
self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
use_stealth = self.old_settings.get('use_stealth')
|
use_stealth = self.old_settings.get('use_stealth')
|
||||||
if use_stealth:
|
if use_stealth:
|
||||||
self.stealth_checkbox.setCheckState(QtCore.Qt.Checked)
|
self.stealth_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
if save_private_key:
|
# Legacy v2 mode is forced on if Stealth is enabled
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(False)
|
||||||
|
if save_private_key and self.old_settings.get('hidservauth_string') != "":
|
||||||
hidservauth_details.show()
|
hidservauth_details.show()
|
||||||
self.hidservauth_copy_button.show()
|
self.hidservauth_copy_button.show()
|
||||||
else:
|
else:
|
||||||
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
if not save_private_key:
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(True)
|
||||||
|
|
||||||
use_autoupdate = self.old_settings.get('use_autoupdate')
|
use_autoupdate = self.old_settings.get('use_autoupdate')
|
||||||
if use_autoupdate:
|
if use_autoupdate:
|
||||||
@ -563,6 +677,51 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
clipboard = self.qtapp.clipboard()
|
clipboard = self.qtapp.clipboard()
|
||||||
clipboard.setText(self.old_settings.get('hidservauth_string'))
|
clipboard.setText(self.old_settings.get('hidservauth_string'))
|
||||||
|
|
||||||
|
def use_legacy_v2_onions_checkbox_clicked(self, checked):
|
||||||
|
"""
|
||||||
|
Show the legacy settings if the legacy mode is enabled.
|
||||||
|
"""
|
||||||
|
if checked:
|
||||||
|
self.save_private_key_widget.show()
|
||||||
|
self.use_stealth_widget.show()
|
||||||
|
else:
|
||||||
|
self.save_private_key_widget.hide()
|
||||||
|
self.use_stealth_widget.hide()
|
||||||
|
|
||||||
|
def save_private_key_checkbox_clicked(self, checked):
|
||||||
|
"""
|
||||||
|
Prevent the v2 legacy mode being switched off if persistence is enabled
|
||||||
|
"""
|
||||||
|
if checked:
|
||||||
|
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(False)
|
||||||
|
else:
|
||||||
|
if not self.stealth_checkbox.isChecked():
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(True)
|
||||||
|
|
||||||
|
def stealth_checkbox_clicked_connect(self, checked):
|
||||||
|
"""
|
||||||
|
Prevent the v2 legacy mode being switched off if stealth is enabled
|
||||||
|
"""
|
||||||
|
if checked:
|
||||||
|
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(False)
|
||||||
|
else:
|
||||||
|
if not self.save_private_key_checkbox.isChecked():
|
||||||
|
self.use_legacy_v2_onions_checkbox.setEnabled(True)
|
||||||
|
|
||||||
|
def downloads_button_clicked(self):
|
||||||
|
"""
|
||||||
|
Browse for a new downloads directory
|
||||||
|
"""
|
||||||
|
downloads_dir = self.downloads_dir_lineedit.text()
|
||||||
|
selected_dir = QtWidgets.QFileDialog.getExistingDirectory(self,
|
||||||
|
strings._('gui_settings_downloads_label', True), downloads_dir)
|
||||||
|
|
||||||
|
if selected_dir:
|
||||||
|
self.common.log('SettingsDialog', 'downloads_button_clicked', 'selected dir: {}'.format(selected_dir))
|
||||||
|
self.downloads_dir_lineedit.setText(selected_dir)
|
||||||
|
|
||||||
def test_tor_clicked(self):
|
def test_tor_clicked(self):
|
||||||
"""
|
"""
|
||||||
Test Tor Settings button clicked. With the given settings, see if we can
|
Test Tor Settings button clicked. With the given settings, see if we can
|
||||||
@ -713,7 +872,7 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
Cancel button clicked.
|
Cancel button clicked.
|
||||||
"""
|
"""
|
||||||
self.common.log('SettingsDialog', 'cancel_clicked')
|
self.common.log('SettingsDialog', 'cancel_clicked')
|
||||||
if not self.onion.is_authenticated():
|
if not self.local_only and not self.onion.is_authenticated():
|
||||||
Alert(self.common, strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning)
|
Alert(self.common, strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
else:
|
else:
|
||||||
@ -736,9 +895,17 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
settings.load() # To get the last update timestamp
|
settings.load() # To get the last update timestamp
|
||||||
|
|
||||||
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
|
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
|
||||||
settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked())
|
|
||||||
settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
|
settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
|
||||||
|
|
||||||
|
# Complicated logic here to force v2 onion mode on or off depending on other settings
|
||||||
|
if self.use_legacy_v2_onions_checkbox.isChecked():
|
||||||
|
use_legacy_v2_onions = True
|
||||||
|
else:
|
||||||
|
use_legacy_v2_onions = False
|
||||||
|
|
||||||
if self.save_private_key_checkbox.isChecked():
|
if self.save_private_key_checkbox.isChecked():
|
||||||
|
# force the legacy mode on
|
||||||
|
use_legacy_v2_onions = True
|
||||||
settings.set('save_private_key', True)
|
settings.set('save_private_key', True)
|
||||||
settings.set('private_key', self.old_settings.get('private_key'))
|
settings.set('private_key', self.old_settings.get('private_key'))
|
||||||
settings.set('slug', self.old_settings.get('slug'))
|
settings.set('slug', self.old_settings.get('slug'))
|
||||||
@ -749,6 +916,21 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||||||
settings.set('slug', '')
|
settings.set('slug', '')
|
||||||
# Also unset the HidServAuth if we are removing our reusable private key
|
# Also unset the HidServAuth if we are removing our reusable private key
|
||||||
settings.set('hidservauth_string', '')
|
settings.set('hidservauth_string', '')
|
||||||
|
|
||||||
|
if use_legacy_v2_onions:
|
||||||
|
settings.set('use_legacy_v2_onions', True)
|
||||||
|
else:
|
||||||
|
settings.set('use_legacy_v2_onions', False)
|
||||||
|
# If we are not using legacy mode, but we previously had persistence turned on, force it off!
|
||||||
|
settings.set('save_private_key', False)
|
||||||
|
settings.set('private_key', '')
|
||||||
|
settings.set('slug', '')
|
||||||
|
# Also unset the HidServAuth if we are removing our reusable private key
|
||||||
|
settings.set('hidservauth_string', '')
|
||||||
|
|
||||||
|
settings.set('downloads_dir', self.downloads_dir_lineedit.text())
|
||||||
|
settings.set('receive_allow_receiver_shutdown', self.receive_allow_receiver_shutdown_checkbox.isChecked())
|
||||||
|
settings.set('public_mode', self.public_mode_checkbox.isChecked())
|
||||||
settings.set('use_stealth', self.stealth_checkbox.isChecked())
|
settings.set('use_stealth', self.stealth_checkbox.isChecked())
|
||||||
# Always unset the HidServAuth if Stealth mode is unset
|
# Always unset the HidServAuth if Stealth mode is unset
|
||||||
if not self.stealth_checkbox.isChecked():
|
if not self.stealth_checkbox.isChecked():
|
||||||
|
418
onionshare_gui/share_mode/__init__.py
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
# -*- 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 threading
|
||||||
|
import os
|
||||||
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
|
from onionshare import strings
|
||||||
|
from onionshare.onion import *
|
||||||
|
from onionshare.common import Common
|
||||||
|
from onionshare.web import Web
|
||||||
|
|
||||||
|
from .file_selection import FileSelection
|
||||||
|
from .downloads import Downloads
|
||||||
|
from ..mode import Mode
|
||||||
|
from ..widgets import Alert
|
||||||
|
|
||||||
|
class ShareMode(Mode):
|
||||||
|
"""
|
||||||
|
Parts of the main window UI for sharing files.
|
||||||
|
"""
|
||||||
|
def init(self):
|
||||||
|
"""
|
||||||
|
Custom initialization for ReceiveMode.
|
||||||
|
"""
|
||||||
|
# Create the Web object
|
||||||
|
self.web = Web(self.common, True, False)
|
||||||
|
|
||||||
|
# File selection
|
||||||
|
self.file_selection = FileSelection(self.common)
|
||||||
|
if self.filenames:
|
||||||
|
for filename in self.filenames:
|
||||||
|
self.file_selection.file_list.add_file(filename)
|
||||||
|
|
||||||
|
# Server status
|
||||||
|
self.server_status.set_mode('share', self.file_selection)
|
||||||
|
self.server_status.server_started.connect(self.file_selection.server_started)
|
||||||
|
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
|
||||||
|
self.server_status.server_stopped.connect(self.update_primary_action)
|
||||||
|
self.server_status.server_canceled.connect(self.file_selection.server_stopped)
|
||||||
|
self.server_status.server_canceled.connect(self.update_primary_action)
|
||||||
|
self.file_selection.file_list.files_updated.connect(self.server_status.update)
|
||||||
|
self.file_selection.file_list.files_updated.connect(self.update_primary_action)
|
||||||
|
# Tell server_status about web, then update
|
||||||
|
self.server_status.web = self.web
|
||||||
|
self.server_status.update()
|
||||||
|
|
||||||
|
# Filesize warning
|
||||||
|
self.filesize_warning = QtWidgets.QLabel()
|
||||||
|
self.filesize_warning.setWordWrap(True)
|
||||||
|
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
|
||||||
|
self.filesize_warning.hide()
|
||||||
|
|
||||||
|
# Downloads
|
||||||
|
self.downloads = Downloads(self.common)
|
||||||
|
self.downloads_in_progress = 0
|
||||||
|
self.downloads_completed = 0
|
||||||
|
|
||||||
|
# Information about share, and show downloads button
|
||||||
|
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'])
|
||||||
|
|
||||||
|
self.info_completed_downloads_count = QtWidgets.QLabel()
|
||||||
|
self.info_completed_downloads_count.setStyleSheet(self.common.css['mode_info_label'])
|
||||||
|
|
||||||
|
self.update_downloads_completed()
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
|
||||||
|
self.info_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.info_layout.addWidget(self.info_label)
|
||||||
|
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)
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
# Primary action layout
|
||||||
|
self.primary_action_layout.addWidget(self.filesize_warning)
|
||||||
|
self.primary_action.hide()
|
||||||
|
self.update_primary_action()
|
||||||
|
|
||||||
|
# Status bar, zip progress bar
|
||||||
|
self._zip_progress_bar = None
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
self.layout.insertLayout(0, self.file_selection)
|
||||||
|
self.layout.insertWidget(0, self.info_widget)
|
||||||
|
|
||||||
|
# Always start with focus on file selection
|
||||||
|
self.file_selection.setFocus()
|
||||||
|
|
||||||
|
def get_stop_server_shutdown_timeout_text(self):
|
||||||
|
"""
|
||||||
|
Return the string to put on the stop server button, if there's a shutdown timeout
|
||||||
|
"""
|
||||||
|
return strings._('gui_share_stop_server_shutdown_timeout', True)
|
||||||
|
|
||||||
|
def timeout_finished_should_stop_server(self):
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
self.server_status.stop_server()
|
||||||
|
self.server_status_label.setText(strings._('close_on_timeout', True))
|
||||||
|
return True
|
||||||
|
# A download is probably still running - hold off on stopping the share
|
||||||
|
else:
|
||||||
|
self.server_status_label.setText(strings._('timeout_download_still_running', True))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_server_custom(self):
|
||||||
|
"""
|
||||||
|
Starting the server.
|
||||||
|
"""
|
||||||
|
# Reset web counters
|
||||||
|
self.web.download_count = 0
|
||||||
|
self.web.error404_count = 0
|
||||||
|
|
||||||
|
# Hide and reset the downloads if we have previously shared
|
||||||
|
self.reset_info_counters()
|
||||||
|
|
||||||
|
def start_server_step2_custom(self):
|
||||||
|
"""
|
||||||
|
Step 2 in starting the server. Zipping up files.
|
||||||
|
"""
|
||||||
|
# Add progress bar to the status bar, indicating the compressing of files.
|
||||||
|
self._zip_progress_bar = ZipProgressBar(self.common, 0)
|
||||||
|
self.filenames = []
|
||||||
|
for index in range(self.file_selection.file_list.count()):
|
||||||
|
self.filenames.append(self.file_selection.file_list.item(index).filename)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def start_server_step3_custom(self):
|
||||||
|
"""
|
||||||
|
Step 3 in starting the server. Remove zip progess bar, and display large filesize
|
||||||
|
warning, if applicable.
|
||||||
|
"""
|
||||||
|
# Remove zip progress bar
|
||||||
|
if self._zip_progress_bar is not None:
|
||||||
|
self.status_bar.removeWidget(self._zip_progress_bar)
|
||||||
|
self._zip_progress_bar = None
|
||||||
|
|
||||||
|
# Warn about sending large files over Tor
|
||||||
|
if self.web.zip_filesize >= 157286400: # 150mb
|
||||||
|
self.filesize_warning.setText(strings._("large_filesize", True))
|
||||||
|
self.filesize_warning.show()
|
||||||
|
|
||||||
|
def start_server_error_custom(self):
|
||||||
|
"""
|
||||||
|
Start server error.
|
||||||
|
"""
|
||||||
|
if self._zip_progress_bar is not None:
|
||||||
|
self.status_bar.removeWidget(self._zip_progress_bar)
|
||||||
|
self._zip_progress_bar = None
|
||||||
|
|
||||||
|
def stop_server_custom(self):
|
||||||
|
"""
|
||||||
|
Stop server.
|
||||||
|
"""
|
||||||
|
# Remove the progress bar
|
||||||
|
if self._zip_progress_bar is not None:
|
||||||
|
self.status_bar.removeWidget(self._zip_progress_bar)
|
||||||
|
self._zip_progress_bar = None
|
||||||
|
|
||||||
|
self.filesize_warning.hide()
|
||||||
|
self.downloads_in_progress = 0
|
||||||
|
self.downloads_completed = 0
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
self.file_selection.file_list.adjustSize()
|
||||||
|
|
||||||
|
def handle_tor_broke_custom(self):
|
||||||
|
"""
|
||||||
|
Connection to Tor broke.
|
||||||
|
"""
|
||||||
|
self.primary_action.hide()
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
def handle_request_load(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_LOAD event.
|
||||||
|
"""
|
||||||
|
self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_download_page_loaded_message', True))
|
||||||
|
|
||||||
|
def handle_request_started(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_STARTED event.
|
||||||
|
"""
|
||||||
|
self.downloads.add(event["data"]["id"], self.web.zip_filesize)
|
||||||
|
self.downloads_in_progress += 1
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
|
||||||
|
self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
|
||||||
|
|
||||||
|
def handle_request_progress(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_PROGRESS event.
|
||||||
|
"""
|
||||||
|
self.downloads.update(event["data"]["id"], event["data"]["bytes"])
|
||||||
|
|
||||||
|
# Is the download complete?
|
||||||
|
if event["data"]["bytes"] == self.web.zip_filesize:
|
||||||
|
self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
|
||||||
|
|
||||||
|
# Update the total 'completed downloads' info
|
||||||
|
self.downloads_completed += 1
|
||||||
|
self.update_downloads_completed()
|
||||||
|
# Update the 'in progress downloads' info
|
||||||
|
self.downloads_in_progress -= 1
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
|
||||||
|
# Close on finish?
|
||||||
|
if self.common.settings.get('close_after_first_download'):
|
||||||
|
self.server_status.stop_server()
|
||||||
|
self.status_bar.clearMessage()
|
||||||
|
self.server_status_label.setText(strings._('closing_automatically', True))
|
||||||
|
else:
|
||||||
|
if self.server_status.status == self.server_status.STATUS_STOPPED:
|
||||||
|
self.downloads.cancel(event["data"]["id"])
|
||||||
|
self.downloads_in_progress = 0
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
|
||||||
|
def handle_request_canceled(self, event):
|
||||||
|
"""
|
||||||
|
Handle REQUEST_CANCELED event.
|
||||||
|
"""
|
||||||
|
self.downloads.cancel(event["data"]["id"])
|
||||||
|
|
||||||
|
# Update the 'in progress downloads' info
|
||||||
|
self.downloads_in_progress -= 1
|
||||||
|
self.update_downloads_in_progress()
|
||||||
|
self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
|
||||||
|
|
||||||
|
def on_reload_settings(self):
|
||||||
|
"""
|
||||||
|
If there were some files listed for sharing, we should be ok to re-enable
|
||||||
|
the 'Start Sharing' button now.
|
||||||
|
"""
|
||||||
|
if self.server_status.file_selection.get_num_files() > 0:
|
||||||
|
self.primary_action.show()
|
||||||
|
self.info_widget.show()
|
||||||
|
|
||||||
|
def update_primary_action(self):
|
||||||
|
# Show or hide primary action layout
|
||||||
|
file_count = self.file_selection.file_list.count()
|
||||||
|
if file_count > 0:
|
||||||
|
self.primary_action.show()
|
||||||
|
self.info_widget.show()
|
||||||
|
|
||||||
|
# Update the file count in the info label
|
||||||
|
total_size_bytes = 0
|
||||||
|
for index in range(self.file_selection.file_list.count()):
|
||||||
|
item = self.file_selection.file_list.item(index)
|
||||||
|
total_size_bytes += item.size_bytes
|
||||||
|
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
|
||||||
|
|
||||||
|
if file_count > 1:
|
||||||
|
self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
|
||||||
|
else:
|
||||||
|
self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.primary_action.hide()
|
||||||
|
self.info_widget.hide()
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
self.downloads_completed = 0
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Update the 'Downloads completed' info widget.
|
||||||
|
"""
|
||||||
|
if self.downloads_completed == 0:
|
||||||
|
image = self.common.get_resource_path('images/share_completed_none.png')
|
||||||
|
else:
|
||||||
|
image = self.common.get_resource_path('images/share_completed.png')
|
||||||
|
self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_completed))
|
||||||
|
self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.downloads_completed))
|
||||||
|
|
||||||
|
def update_downloads_in_progress(self):
|
||||||
|
"""
|
||||||
|
Update the 'Downloads in progress' info widget.
|
||||||
|
"""
|
||||||
|
if self.downloads_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_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))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compute_total_size(filenames):
|
||||||
|
total_size = 0
|
||||||
|
for filename in filenames:
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
total_size += os.path.getsize(filename)
|
||||||
|
if os.path.isdir(filename):
|
||||||
|
total_size += Common.dir_size(filename)
|
||||||
|
return total_size
|
||||||
|
|
||||||
|
|
||||||
|
class ZipProgressBar(QtWidgets.QProgressBar):
|
||||||
|
update_processed_size_signal = QtCore.pyqtSignal(int)
|
||||||
|
|
||||||
|
def __init__(self, common, total_files_size):
|
||||||
|
super(ZipProgressBar, self).__init__()
|
||||||
|
self.common = common
|
||||||
|
|
||||||
|
self.setMaximumHeight(20)
|
||||||
|
self.setMinimumWidth(200)
|
||||||
|
self.setValue(0)
|
||||||
|
self.setFormat(strings._('zip_progress_bar_format'))
|
||||||
|
self.setStyleSheet(self.common.css['share_zip_progess_bar'])
|
||||||
|
|
||||||
|
self._total_files_size = total_files_size
|
||||||
|
self._processed_size = 0
|
||||||
|
|
||||||
|
self.update_processed_size_signal.connect(self.update_processed_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_files_size(self):
|
||||||
|
return self._total_files_size
|
||||||
|
|
||||||
|
@total_files_size.setter
|
||||||
|
def total_files_size(self, val):
|
||||||
|
self._total_files_size = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def processed_size(self):
|
||||||
|
return self._processed_size
|
||||||
|
|
||||||
|
@processed_size.setter
|
||||||
|
def processed_size(self, val):
|
||||||
|
self.update_processed_size(val)
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.setValue(100)
|
||||||
|
else:
|
||||||
|
self.setValue(0)
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -18,13 +18,12 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
from onionshare import strings
|
from onionshare import strings
|
||||||
|
|
||||||
class Download(object):
|
|
||||||
|
|
||||||
|
class Download(object):
|
||||||
def __init__(self, common, download_id, total_bytes):
|
def __init__(self, common, download_id, total_bytes):
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
@ -33,20 +32,7 @@ class Download(object):
|
|||||||
self.total_bytes = total_bytes
|
self.total_bytes = total_bytes
|
||||||
self.downloaded_bytes = 0
|
self.downloaded_bytes = 0
|
||||||
|
|
||||||
# make a new progress bar
|
# Progress bar
|
||||||
cssStyleData ="""
|
|
||||||
QProgressBar {
|
|
||||||
border: 1px solid #4e064f;
|
|
||||||
background-color: #ffffff !important;
|
|
||||||
text-align: center;
|
|
||||||
color: #9b9b9b;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QProgressBar::chunk {
|
|
||||||
background-color: #4e064f;
|
|
||||||
width: 10px;
|
|
||||||
}"""
|
|
||||||
self.progress_bar = QtWidgets.QProgressBar()
|
self.progress_bar = QtWidgets.QProgressBar()
|
||||||
self.progress_bar.setTextVisible(True)
|
self.progress_bar.setTextVisible(True)
|
||||||
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||||
@ -54,10 +40,10 @@ class Download(object):
|
|||||||
self.progress_bar.setMinimum(0)
|
self.progress_bar.setMinimum(0)
|
||||||
self.progress_bar.setMaximum(total_bytes)
|
self.progress_bar.setMaximum(total_bytes)
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
self.progress_bar.setStyleSheet(cssStyleData)
|
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
|
||||||
self.progress_bar.total_bytes = total_bytes
|
self.progress_bar.total_bytes = total_bytes
|
||||||
|
|
||||||
# start at 0
|
# Start at 0
|
||||||
self.update(0)
|
self.update(0)
|
||||||
|
|
||||||
def update(self, downloaded_bytes):
|
def update(self, downloaded_bytes):
|
||||||
@ -65,7 +51,7 @@ class Download(object):
|
|||||||
|
|
||||||
self.progress_bar.setValue(downloaded_bytes)
|
self.progress_bar.setValue(downloaded_bytes)
|
||||||
if downloaded_bytes == self.progress_bar.total_bytes:
|
if downloaded_bytes == self.progress_bar.total_bytes:
|
||||||
pb_fmt = strings._('gui_download_progress_complete').format(
|
pb_fmt = strings._('gui_download_upload_progress_complete').format(
|
||||||
self.common.format_seconds(time.time() - self.started))
|
self.common.format_seconds(time.time() - self.started))
|
||||||
else:
|
else:
|
||||||
elapsed = time.time() - self.started
|
elapsed = time.time() - self.started
|
||||||
@ -73,10 +59,10 @@ class Download(object):
|
|||||||
# Wait a couple of seconds for the download rate to stabilize.
|
# Wait a couple of seconds for the download rate to stabilize.
|
||||||
# This prevents a "Windows copy dialog"-esque experience at
|
# This prevents a "Windows copy dialog"-esque experience at
|
||||||
# the beginning of the download.
|
# the beginning of the download.
|
||||||
pb_fmt = strings._('gui_download_progress_starting').format(
|
pb_fmt = strings._('gui_download_upload_progress_starting').format(
|
||||||
self.common.human_readable_filesize(downloaded_bytes))
|
self.common.human_readable_filesize(downloaded_bytes))
|
||||||
else:
|
else:
|
||||||
pb_fmt = strings._('gui_download_progress_eta').format(
|
pb_fmt = strings._('gui_download_upload_progress_eta').format(
|
||||||
self.common.human_readable_filesize(downloaded_bytes),
|
self.common.human_readable_filesize(downloaded_bytes),
|
||||||
self.estimated_time_remaining)
|
self.estimated_time_remaining)
|
||||||
|
|
||||||
@ -92,64 +78,69 @@ class Download(object):
|
|||||||
self.started)
|
self.started)
|
||||||
|
|
||||||
|
|
||||||
class Downloads(QtWidgets.QWidget):
|
class Downloads(QtWidgets.QScrollArea):
|
||||||
"""
|
"""
|
||||||
The downloads chunk of the GUI. This lists all of the active download
|
The downloads chunk of the GUI. This lists all of the active download
|
||||||
progress bars.
|
progress bars.
|
||||||
"""
|
"""
|
||||||
def __init__(self, common):
|
def __init__(self, common):
|
||||||
super(Downloads, self).__init__()
|
super(Downloads, self).__init__()
|
||||||
|
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
self.downloads = {}
|
self.downloads = {}
|
||||||
|
|
||||||
self.downloads_container = QtWidgets.QScrollArea()
|
self.setWindowTitle(strings._('gui_downloads', True))
|
||||||
self.downloads_container.setWidget(self)
|
self.setWidgetResizable(True)
|
||||||
self.downloads_container.setWindowTitle(strings._('gui_downloads', True))
|
self.setMaximumHeight(600)
|
||||||
self.downloads_container.setWidgetResizable(True)
|
self.setMinimumHeight(150)
|
||||||
self.downloads_container.setMaximumHeight(600)
|
self.setMinimumWidth(350)
|
||||||
self.downloads_container.setMinimumHeight(150)
|
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
|
||||||
self.downloads_container.setMinimumWidth(350)
|
self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
|
||||||
self.downloads_container.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
|
self.vbar = self.verticalScrollBar()
|
||||||
self.downloads_container.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
|
|
||||||
self.downloads_container.vbar = self.downloads_container.verticalScrollBar()
|
|
||||||
|
|
||||||
self.downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
|
downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
|
||||||
self.downloads_label.setStyleSheet('QLabel { font-weight: bold; font-size 14px; text-align: center; }')
|
downloads_label.setStyleSheet(self.common.css['downloads_uploads_label'])
|
||||||
self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True))
|
self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True))
|
||||||
|
|
||||||
self.downloads_layout = QtWidgets.QVBoxLayout()
|
self.downloads_layout = QtWidgets.QVBoxLayout()
|
||||||
|
|
||||||
self.layout = QtWidgets.QVBoxLayout()
|
widget = QtWidgets.QWidget()
|
||||||
self.layout.addWidget(self.downloads_label)
|
layout = QtWidgets.QVBoxLayout()
|
||||||
self.layout.addWidget(self.no_downloads_label)
|
layout.addWidget(downloads_label)
|
||||||
self.layout.addLayout(self.downloads_layout)
|
layout.addWidget(self.no_downloads_label)
|
||||||
self.layout.addStretch()
|
layout.addLayout(self.downloads_layout)
|
||||||
self.setLayout(self.layout)
|
layout.addStretch()
|
||||||
|
widget.setLayout(layout)
|
||||||
|
self.setWidget(widget)
|
||||||
|
|
||||||
def add_download(self, download_id, total_bytes):
|
def add(self, download_id, total_bytes):
|
||||||
"""
|
"""
|
||||||
Add a new download progress bar.
|
Add a new download progress bar.
|
||||||
"""
|
"""
|
||||||
# add it to the list
|
# Hide the no_downloads_label
|
||||||
|
self.no_downloads_label.hide()
|
||||||
|
|
||||||
|
# Add it to the list
|
||||||
download = Download(self.common, download_id, total_bytes)
|
download = Download(self.common, download_id, total_bytes)
|
||||||
self.downloads[download_id] = download
|
self.downloads[download_id] = download
|
||||||
self.downloads_layout.addWidget(download.progress_bar)
|
self.downloads_layout.addWidget(download.progress_bar)
|
||||||
|
|
||||||
def update_download(self, download_id, downloaded_bytes):
|
# 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.
|
Update the progress of a download progress bar.
|
||||||
"""
|
"""
|
||||||
self.downloads[download_id].update(downloaded_bytes)
|
self.downloads[download_id].update(downloaded_bytes)
|
||||||
|
|
||||||
def cancel_download(self, download_id):
|
def cancel(self, download_id):
|
||||||
"""
|
"""
|
||||||
Update a download progress bar to show that it has been canceled.
|
Update a download progress bar to show that it has been canceled.
|
||||||
"""
|
"""
|
||||||
self.downloads[download_id].cancel()
|
self.downloads[download_id].cancel()
|
||||||
|
|
||||||
def reset_downloads(self):
|
def reset(self):
|
||||||
"""
|
"""
|
||||||
Reset the downloads back to zero
|
Reset the downloads back to zero
|
||||||
"""
|
"""
|
||||||
@ -157,3 +148,6 @@ class Downloads(QtWidgets.QWidget):
|
|||||||
self.downloads_layout.removeWidget(download.progress_bar)
|
self.downloads_layout.removeWidget(download.progress_bar)
|
||||||
download.progress_bar.close()
|
download.progress_bar.close()
|
||||||
self.downloads = {}
|
self.downloads = {}
|
||||||
|
|
||||||
|
self.no_downloads_label.show()
|
||||||
|
self.resize(self.sizeHint())
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -19,10 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
from .alert import Alert
|
|
||||||
|
|
||||||
from onionshare import strings
|
from onionshare import strings
|
||||||
|
|
||||||
|
from ..widgets import Alert, AddFileDialog
|
||||||
|
|
||||||
class DropHereLabel(QtWidgets.QLabel):
|
class DropHereLabel(QtWidgets.QLabel):
|
||||||
"""
|
"""
|
||||||
When there are no files or folders in the FileList yet, display the
|
When there are no files or folders in the FileList yet, display the
|
||||||
@ -41,7 +42,7 @@ class DropHereLabel(QtWidgets.QLabel):
|
|||||||
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png'))))
|
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png'))))
|
||||||
else:
|
else:
|
||||||
self.setText(strings._('gui_drag_and_drop', True))
|
self.setText(strings._('gui_drag_and_drop', True))
|
||||||
self.setStyleSheet('color: #999999;')
|
self.setStyleSheet(self.common.css['share_file_selection_drop_here_label'])
|
||||||
|
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class DropCountLabel(QtWidgets.QLabel):
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self.setAlignment(QtCore.Qt.AlignCenter)
|
self.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.setText(strings._('gui_drag_and_drop', True))
|
self.setText(strings._('gui_drag_and_drop', True))
|
||||||
self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;')
|
self.setStyleSheet(self.common.css['share_file_selection_drop_count_label'])
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
@ -157,7 +158,7 @@ class FileList(QtWidgets.QListWidget):
|
|||||||
dragEnterEvent for dragging files and directories into the widget.
|
dragEnterEvent for dragging files and directories into the widget.
|
||||||
"""
|
"""
|
||||||
if event.mimeData().hasUrls:
|
if event.mimeData().hasUrls:
|
||||||
self.setStyleSheet('FileList { border: 3px solid #538ad0; }')
|
self.setStyleSheet(self.common.css['share_file_list_drag_enter'])
|
||||||
count = len(event.mimeData().urls())
|
count = len(event.mimeData().urls())
|
||||||
self.drop_count.setText('+{}'.format(count))
|
self.drop_count.setText('+{}'.format(count))
|
||||||
|
|
||||||
@ -172,7 +173,7 @@ class FileList(QtWidgets.QListWidget):
|
|||||||
"""
|
"""
|
||||||
dragLeaveEvent for dragging files and directories into the widget.
|
dragLeaveEvent for dragging files and directories into the widget.
|
||||||
"""
|
"""
|
||||||
self.setStyleSheet('FileList { border: none; }')
|
self.setStyleSheet(self.common.css['share_file_list_drag_leave'])
|
||||||
self.drop_count.hide()
|
self.drop_count.hide()
|
||||||
event.accept()
|
event.accept()
|
||||||
self.update()
|
self.update()
|
||||||
@ -200,7 +201,7 @@ class FileList(QtWidgets.QListWidget):
|
|||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
self.setStyleSheet('border: none;')
|
self.setStyleSheet(self.common.css['share_file_list_drag_leave'])
|
||||||
self.drop_count.hide()
|
self.drop_count.hide()
|
||||||
|
|
||||||
self.files_dropped.emit()
|
self.files_dropped.emit()
|
||||||
@ -237,7 +238,7 @@ class FileList(QtWidgets.QListWidget):
|
|||||||
# Item's filename attribute and size labels
|
# Item's filename attribute and size labels
|
||||||
item.filename = filename
|
item.filename = filename
|
||||||
item_size = QtWidgets.QLabel(size_readable)
|
item_size = QtWidgets.QLabel(size_readable)
|
||||||
item_size.setStyleSheet('QLabel { color: #666666; font-size: 11px; }')
|
item_size.setStyleSheet(self.common.css['share_file_list_item_size'])
|
||||||
|
|
||||||
item.basename = os.path.basename(filename.rstrip('/'))
|
item.basename = os.path.basename(filename.rstrip('/'))
|
||||||
# Use the basename as the method with which to sort the list
|
# Use the basename as the method with which to sort the list
|
||||||
@ -339,7 +340,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
|
|||||||
"""
|
"""
|
||||||
Add button clicked.
|
Add button clicked.
|
||||||
"""
|
"""
|
||||||
file_dialog = FileDialog(caption=strings._('gui_choose_items', True))
|
file_dialog = AddFileDialog(self.common, caption=strings._('gui_choose_items', True))
|
||||||
if file_dialog.exec_() == QtWidgets.QDialog.Accepted:
|
if file_dialog.exec_() == QtWidgets.QDialog.Accepted:
|
||||||
for filename in file_dialog.selectedFiles():
|
for filename in file_dialog.selectedFiles():
|
||||||
self.file_list.add_file(filename)
|
self.file_list.add_file(filename)
|
||||||
@ -387,22 +388,3 @@ class FileSelection(QtWidgets.QVBoxLayout):
|
|||||||
Set the Qt app focus on the file selection box.
|
Set the Qt app focus on the file selection box.
|
||||||
"""
|
"""
|
||||||
self.file_list.setFocus()
|
self.file_list.setFocus()
|
||||||
|
|
||||||
class FileDialog(QtWidgets.QFileDialog):
|
|
||||||
"""
|
|
||||||
Overridden version of QFileDialog which allows us to select
|
|
||||||
folders as well as, or instead of, files.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
QtWidgets.QFileDialog.__init__(self, *args, **kwargs)
|
|
||||||
self.setOption(self.DontUseNativeDialog, True)
|
|
||||||
self.setOption(self.ReadOnly, True)
|
|
||||||
self.setOption(self.ShowDirsOnly, False)
|
|
||||||
self.setFileMode(self.ExistingFiles)
|
|
||||||
tree_view = self.findChild(QtWidgets.QTreeView)
|
|
||||||
tree_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
|
||||||
list_view = self.findChild(QtWidgets.QListView, "listView")
|
|
||||||
list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
|
||||||
|
|
||||||
def accept(self):
|
|
||||||
QtWidgets.QDialog.accept(self)
|
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
|
|||||||
from onionshare import strings
|
from onionshare import strings
|
||||||
from onionshare.onion import *
|
from onionshare.onion import *
|
||||||
|
|
||||||
from .alert import Alert
|
from .widgets import Alert
|
||||||
|
|
||||||
class TorConnectionDialog(QtWidgets.QProgressDialog):
|
class TorConnectionDialog(QtWidgets.QProgressDialog):
|
||||||
"""
|
"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -38,3 +38,28 @@ class Alert(QtWidgets.QMessageBox):
|
|||||||
|
|
||||||
if autostart:
|
if autostart:
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
class AddFileDialog(QtWidgets.QFileDialog):
|
||||||
|
"""
|
||||||
|
Overridden version of QFileDialog which allows us to select folders as well
|
||||||
|
as, or instead of, files. For adding files/folders to share.
|
||||||
|
"""
|
||||||
|
def __init__(self, common, *args, **kwargs):
|
||||||
|
QtWidgets.QFileDialog.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
self.common = common
|
||||||
|
self.common.log('AddFileDialog', '__init__')
|
||||||
|
|
||||||
|
self.setOption(self.DontUseNativeDialog, True)
|
||||||
|
self.setOption(self.ReadOnly, True)
|
||||||
|
self.setOption(self.ShowDirsOnly, False)
|
||||||
|
self.setFileMode(self.ExistingFiles)
|
||||||
|
tree_view = self.findChild(QtWidgets.QTreeView)
|
||||||
|
tree_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||||
|
list_view = self.findChild(QtWidgets.QListView, "listView")
|
||||||
|
list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
self.common.log('AddFileDialog', 'accept')
|
||||||
|
QtWidgets.QDialog.accept(self)
|
2
setup.py
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
BIN
share/images/open_folder.png
Normal file
After Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 411 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 646 B |
Before Width: | Height: | Size: 437 B After Width: | Height: | Size: 437 B |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 412 B |
BIN
share/images/upload_window_gray.png
Normal file
After Width: | Height: | Size: 298 B |
BIN
share/images/upload_window_green.png
Normal file
After Width: | Height: | Size: 483 B |
@ -2,22 +2,15 @@
|
|||||||
"config_onion_service": "Nastavuji onion service na portu {0:d}.",
|
"config_onion_service": "Nastavuji onion service na portu {0:d}.",
|
||||||
"preparing_files": "Připravuji soubory ke sdílení.",
|
"preparing_files": "Připravuji soubory ke sdílení.",
|
||||||
"wait_for_hs": "Čekám na HS až bude připravena:",
|
"wait_for_hs": "Čekám na HS až bude připravena:",
|
||||||
"wait_for_hs_trying": "Zkouším...",
|
|
||||||
"wait_for_hs_nope": "Ještě nepřipraven.",
|
|
||||||
"wait_for_hs_yup": "Připraven!",
|
|
||||||
"give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:",
|
"give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:",
|
||||||
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
|
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
|
||||||
"ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru",
|
"ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru",
|
||||||
"not_a_file": "{0:s} není soubor.",
|
"not_a_file": "{0:s} není soubor.",
|
||||||
"download_page_loaded": "Download page loaded",
|
|
||||||
"other_page_loaded": "URL loaded",
|
"other_page_loaded": "URL loaded",
|
||||||
"closing_automatically": "Zastavuji automaticky, protože stahování skončilo",
|
"closing_automatically": "Zastavuji automaticky, protože stahování skončilo",
|
||||||
"large_filesize": "Varování: Posílání velkých souborů může trvat hodiny",
|
"large_filesize": "Varování: Posílání velkých souborů může trvat hodiny",
|
||||||
"error_tails_invalid_port": "Nesprávná hodnota, port musí být celé číslo",
|
|
||||||
"error_tails_unknown_root": "Neznámá chyba s Tails root procesem",
|
|
||||||
"help_local_only": "Nepoužívat Tor: jen pro vývoj",
|
"help_local_only": "Nepoužívat Tor: jen pro vývoj",
|
||||||
"help_stay_open": "Nechat běžet onion service po skončení stahování",
|
"help_stay_open": "Nechat běžet onion service po skončení stahování",
|
||||||
"help_transparent_torification": "My system is transparently torified",
|
|
||||||
"help_stealth": "Create stealth onion service (advanced)",
|
"help_stealth": "Create stealth onion service (advanced)",
|
||||||
"help_debug": "Zaznamenat chyby na disk",
|
"help_debug": "Zaznamenat chyby na disk",
|
||||||
"help_filename": "Seznam souborů a složek ke sdílení",
|
"help_filename": "Seznam souborů a složek ke sdílení",
|
||||||
@ -25,34 +18,27 @@
|
|||||||
"gui_add": "Přidat",
|
"gui_add": "Přidat",
|
||||||
"gui_delete": "Smazat",
|
"gui_delete": "Smazat",
|
||||||
"gui_choose_items": "Vybrat",
|
"gui_choose_items": "Vybrat",
|
||||||
"gui_start_server": "Spustit sdílení",
|
"gui_share_start_server": "Spustit sdílení",
|
||||||
"gui_stop_server": "Zastavit sdílení",
|
"gui_share_stop_server": "Zastavit sdílení",
|
||||||
"gui_copy_url": "Kopírovat URL",
|
"gui_copy_url": "Kopírovat URL",
|
||||||
"gui_copy_hidservauth": "Kopírovat HidServAuth",
|
"gui_copy_hidservauth": "Kopírovat HidServAuth",
|
||||||
"gui_downloads": "Stahování:",
|
"gui_downloads": "Stahování:",
|
||||||
"gui_canceled": "Zrušeno",
|
"gui_canceled": "Zrušeno",
|
||||||
"gui_copied_url": "URL zkopírováno do schránky",
|
"gui_copied_url": "URL zkopírováno do schránky",
|
||||||
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
|
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
|
||||||
"gui_starting_server1": "Spouštím Tor onion service...",
|
|
||||||
"gui_starting_server2": "Zpracovávám soubory...",
|
|
||||||
"gui_please_wait": "Prosím čekejte...",
|
"gui_please_wait": "Prosím čekejte...",
|
||||||
"error_hs_dir_cannot_create": "Nejde vytvořit složka onion service {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "nejde zapisovat do složky onion service {0:s}",
|
|
||||||
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
||||||
"gui_download_progress_complete": "%p%, Uplynulý čas: {0:s}",
|
"gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}",
|
||||||
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
|
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
|
||||||
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
||||||
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
||||||
"gui_quit_warning": "Jste si jistí, že chcete odejít?\nURL, kterou sdílíte poté nebude existovat.",
|
"gui_share_quit_warning": "Jste si jistí, že chcete odejít?\nURL, kterou sdílíte poté nebude existovat.",
|
||||||
"gui_quit_warning_quit": "Zavřít",
|
"gui_quit_warning_quit": "Zavřít",
|
||||||
"gui_quit_warning_dont_quit": "Zůstat",
|
"gui_quit_warning_dont_quit": "Zůstat",
|
||||||
"error_rate_limit": "Útočník možná zkouší uhodnout vaši URL. Abychom tomu předešli, OnionShare automaticky zastavil server. Pro sdílení souborů ho musíte spustit znovu a sdílet novou URL.",
|
"error_rate_limit": "Útočník možná zkouší uhodnout vaši URL. Abychom tomu předešli, OnionShare automaticky zastavil server. Pro sdílení souborů ho musíte spustit znovu a sdílet novou URL.",
|
||||||
"zip_progress_bar_format": "Zpracovávám soubory: %p%",
|
"zip_progress_bar_format": "Zpracovávám soubory: %p%",
|
||||||
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
||||||
"error_ephemeral_not_supported": "OnionShare vyžaduje nejméně Tor 0.2.7.1 a nejméně python3-stem 1.4.0.",
|
"error_ephemeral_not_supported": "OnionShare vyžaduje nejméně Tor 0.2.7.1 a nejméně python3-stem 1.4.0.",
|
||||||
"gui_menu_file_menu": "&File",
|
|
||||||
"gui_menu_settings_action": "&Settings",
|
|
||||||
"gui_menu_quit_action": "&Quit",
|
|
||||||
"gui_settings_window_title": "Nastavení",
|
"gui_settings_window_title": "Nastavení",
|
||||||
"gui_settings_connection_type_label": "Jak by se měl OnionShare připojit k Toru?",
|
"gui_settings_connection_type_label": "Jak by se měl OnionShare připojit k Toru?",
|
||||||
"gui_settings_connection_type_automatic_option": "Zkusit automatické nastavení s Tor Browserem",
|
"gui_settings_connection_type_automatic_option": "Zkusit automatické nastavení s Tor Browserem",
|
||||||
@ -63,10 +49,7 @@
|
|||||||
"gui_settings_authenticate_label": "Autentizační možnosti Toru",
|
"gui_settings_authenticate_label": "Autentizační možnosti Toru",
|
||||||
"gui_settings_authenticate_no_auth_option": "Žádná autentizace ani cookie autentizace",
|
"gui_settings_authenticate_no_auth_option": "Žádná autentizace ani cookie autentizace",
|
||||||
"gui_settings_authenticate_password_option": "Heslo",
|
"gui_settings_authenticate_password_option": "Heslo",
|
||||||
"gui_settings_authenticate_cookie_option": "Cookie",
|
|
||||||
"gui_settings_password_label": "Heslo",
|
"gui_settings_password_label": "Heslo",
|
||||||
"gui_settings_cookie_label": "Cesta ke cookie",
|
|
||||||
"gui_settings_button_test": "Test Settings",
|
|
||||||
"gui_settings_button_save": "Uložit",
|
"gui_settings_button_save": "Uložit",
|
||||||
"gui_settings_button_cancel": "Zrušit",
|
"gui_settings_button_cancel": "Zrušit",
|
||||||
"settings_saved": "Nastavení uloženo do {}",
|
"settings_saved": "Nastavení uloženo do {}",
|
||||||
|
@ -2,23 +2,17 @@
|
|||||||
"config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
|
"config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
|
||||||
"preparing_files": "Forbereder filer som skal deles.",
|
"preparing_files": "Forbereder filer som skal deles.",
|
||||||
"wait_for_hs": "Venter på at HS bliver klar:",
|
"wait_for_hs": "Venter på at HS bliver klar:",
|
||||||
"wait_for_hs_trying": "Prøver...",
|
|
||||||
"wait_for_hs_nope": "Endnu ikke klar.",
|
|
||||||
"wait_for_hs_yup": "Klar!",
|
|
||||||
"give_this_url": "Giv denne URL til personen du sender filen til:",
|
"give_this_url": "Giv denne URL til personen du sender filen til:",
|
||||||
"give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
|
"give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
|
||||||
"ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
|
"ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
|
||||||
"not_a_file": "{0:s} er ikke en gyldig fil.",
|
"not_a_file": "{0:s} er ikke en gyldig fil.",
|
||||||
"not_a_readable_file": "{0:s} er ikke en læsbar fil.",
|
"not_a_readable_file": "{0:s} er ikke en læsbar fil.",
|
||||||
"no_available_port": "Kunne ikke starte onion-tjenesten da der ikke var nogen tilgængelig port.",
|
"no_available_port": "Kunne ikke starte onion-tjenesten da der ikke var nogen tilgængelig port.",
|
||||||
"download_page_loaded": "Downloadside indlæst",
|
|
||||||
"other_page_loaded": "URL indlæst",
|
"other_page_loaded": "URL indlæst",
|
||||||
"close_on_timeout": "Lukker automatisk da timeout er nået",
|
"close_on_timeout": "Lukker automatisk da timeout er nået",
|
||||||
"closing_automatically": "Lukker automatisk da download er færdig",
|
"closing_automatically": "Lukker automatisk da download er færdig",
|
||||||
"timeout_download_still_running": "Venter på at download skal blive færdig inden automatisk stop",
|
"timeout_download_still_running": "Venter på at download skal blive færdig inden automatisk stop",
|
||||||
"large_filesize": "Advarsel: Det kan tage timer at sende store filer",
|
"large_filesize": "Advarsel: Det kan tage timer at sende store filer",
|
||||||
"error_tails_invalid_port": "Ugyldig værdi, port skal være et heltal",
|
|
||||||
"error_tails_unknown_root": "Ukendt fejl med Tails-rodproces",
|
|
||||||
"systray_menu_exit": "Afslut",
|
"systray_menu_exit": "Afslut",
|
||||||
"systray_download_started_title": "OnionShare-download startet",
|
"systray_download_started_title": "OnionShare-download startet",
|
||||||
"systray_download_started_message": "En bruger startede download af dine filer",
|
"systray_download_started_message": "En bruger startede download af dine filer",
|
||||||
@ -29,7 +23,6 @@
|
|||||||
"help_local_only": "Undlad at bruge tor: kun til udvikling",
|
"help_local_only": "Undlad at bruge tor: kun til udvikling",
|
||||||
"help_stay_open": "Hold onion-tjeneste kørende efter download er færdig",
|
"help_stay_open": "Hold onion-tjeneste kørende efter download er færdig",
|
||||||
"help_shutdown_timeout": "Luk onion-tjenesten efter N sekunder",
|
"help_shutdown_timeout": "Luk onion-tjenesten efter N sekunder",
|
||||||
"help_transparent_torification": "Mit system er gennemsigtigt torifiseret",
|
|
||||||
"help_stealth": "Opret usynlig onion-tjeneste (avanceret)",
|
"help_stealth": "Opret usynlig onion-tjeneste (avanceret)",
|
||||||
"help_debug": "Log programfejl til stdout, og log webfejl til disk",
|
"help_debug": "Log programfejl til stdout, og log webfejl til disk",
|
||||||
"help_filename": "Liste over filer eller mapper som skal deles",
|
"help_filename": "Liste over filer eller mapper som skal deles",
|
||||||
@ -38,25 +31,21 @@
|
|||||||
"gui_add": "Tilføj",
|
"gui_add": "Tilføj",
|
||||||
"gui_delete": "Slet",
|
"gui_delete": "Slet",
|
||||||
"gui_choose_items": "Vælg",
|
"gui_choose_items": "Vælg",
|
||||||
"gui_start_server": "Start deling",
|
"gui_share_start_server": "Start deling",
|
||||||
"gui_stop_server": "Stop deling",
|
"gui_share_stop_server": "Stop deling",
|
||||||
"gui_copy_url": "Kopiér URL",
|
"gui_copy_url": "Kopiér URL",
|
||||||
"gui_copy_hidservauth": "Kopiér HidServAuth",
|
"gui_copy_hidservauth": "Kopiér HidServAuth",
|
||||||
"gui_downloads": "Downloads:",
|
"gui_downloads": "Downloads:",
|
||||||
"gui_canceled": "Annulleret",
|
"gui_canceled": "Annulleret",
|
||||||
"gui_copied_url": "Kopierede URL til udklipsholder",
|
"gui_copied_url": "Kopierede URL til udklipsholder",
|
||||||
"gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
|
"gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
|
||||||
"gui_starting_server1": "Starter Tor onion-tjeneste...",
|
|
||||||
"gui_starting_server2": "Databehandler filer...",
|
|
||||||
"gui_please_wait": "Vent venligst...",
|
"gui_please_wait": "Vent venligst...",
|
||||||
"error_hs_dir_cannot_create": "Kan ikke oprette onion-tjenestens mappe {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "onion-tjenestens mappe {0:s} er skrivebeskyttet",
|
|
||||||
"using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
|
"using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
|
||||||
"gui_download_progress_complete": "%p%, tid forløbet: {0:s}",
|
"gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}",
|
||||||
"gui_download_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
|
"gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
|
||||||
"gui_download_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
|
"gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
|
||||||
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
||||||
"gui_quit_warning": "Er du sikker på, at du vil afslutte?\nURL'en som du deler vil ikke eksistere længere.",
|
"gui_share_quit_warning": "Er du sikker på, at du vil afslutte?\nURL'en som du deler vil ikke eksistere længere.",
|
||||||
"gui_quit_warning_quit": "Afslut",
|
"gui_quit_warning_quit": "Afslut",
|
||||||
"gui_quit_warning_dont_quit": "Afslut ikke",
|
"gui_quit_warning_dont_quit": "Afslut ikke",
|
||||||
"error_rate_limit": "En angriber forsøger måske at gætte din URL. For at forhindre det, har OnionShare automatisk stoppet serveren. For at dele filerne skal du starte den igen og dele den nye URL.",
|
"error_rate_limit": "En angriber forsøger måske at gætte din URL. For at forhindre det, har OnionShare automatisk stoppet serveren. For at dele filerne skal du starte den igen og dele den nye URL.",
|
||||||
@ -75,7 +64,6 @@
|
|||||||
"gui_settings_autoupdate_check_button": "Søg efter opdateringer",
|
"gui_settings_autoupdate_check_button": "Søg efter opdateringer",
|
||||||
"gui_settings_sharing_label": "Valgmuligheder for deling",
|
"gui_settings_sharing_label": "Valgmuligheder for deling",
|
||||||
"gui_settings_close_after_first_download_option": "Stop deling efter første download",
|
"gui_settings_close_after_first_download_option": "Stop deling efter første download",
|
||||||
"gui_settings_systray_notifications": "Vis skrivebordsnotifikationer",
|
|
||||||
"gui_settings_connection_type_label": "Hvordan skal OnionShare oprette forbindelse til Tor?",
|
"gui_settings_connection_type_label": "Hvordan skal OnionShare oprette forbindelse til Tor?",
|
||||||
"gui_settings_connection_type_bundled_option": "Brug Tor som er bundet med OnionShare",
|
"gui_settings_connection_type_bundled_option": "Brug Tor som er bundet med OnionShare",
|
||||||
"gui_settings_connection_type_automatic_option": "Prøv automatisk konfiguration med Tor Browser",
|
"gui_settings_connection_type_automatic_option": "Prøv automatisk konfiguration med Tor Browser",
|
||||||
@ -88,9 +76,7 @@
|
|||||||
"gui_settings_authenticate_label": "Valgmuligheder for Tor-autentifikation",
|
"gui_settings_authenticate_label": "Valgmuligheder for Tor-autentifikation",
|
||||||
"gui_settings_authenticate_no_auth_option": "Ingen autentifikation, eller cookieautentifikation",
|
"gui_settings_authenticate_no_auth_option": "Ingen autentifikation, eller cookieautentifikation",
|
||||||
"gui_settings_authenticate_password_option": "Adgangskode",
|
"gui_settings_authenticate_password_option": "Adgangskode",
|
||||||
"gui_settings_authenticate_cookie_option": "Cookie",
|
|
||||||
"gui_settings_password_label": "Adgangskode",
|
"gui_settings_password_label": "Adgangskode",
|
||||||
"gui_settings_cookie_label": "Cookiesti",
|
|
||||||
"gui_settings_tor_bridges": "Understøttelse af Tor-bro",
|
"gui_settings_tor_bridges": "Understøttelse af Tor-bro",
|
||||||
"gui_settings_tor_bridges_no_bridges_radio_option": "Brug ikke broer",
|
"gui_settings_tor_bridges_no_bridges_radio_option": "Brug ikke broer",
|
||||||
"gui_settings_tor_bridges_obfs4_radio_option": "Brug indbygget obfs4 udskiftelige transporter",
|
"gui_settings_tor_bridges_obfs4_radio_option": "Brug indbygget obfs4 udskiftelige transporter",
|
||||||
@ -101,7 +87,6 @@
|
|||||||
"gui_settings_button_save": "Gem",
|
"gui_settings_button_save": "Gem",
|
||||||
"gui_settings_button_cancel": "Annuller",
|
"gui_settings_button_cancel": "Annuller",
|
||||||
"gui_settings_button_help": "Hjælp",
|
"gui_settings_button_help": "Hjælp",
|
||||||
"gui_settings_shutdown_timeout_choice": "Sæt timer til automatisk stop?",
|
|
||||||
"gui_settings_shutdown_timeout": "Stop delingen ved:",
|
"gui_settings_shutdown_timeout": "Stop delingen ved:",
|
||||||
"settings_saved": "Indstillinger gemt til {}",
|
"settings_saved": "Indstillinger gemt til {}",
|
||||||
"settings_error_unknown": "Kan ikke oprette forbindelse til Tor-kontroller da indstillingerne ikke giver mening.",
|
"settings_error_unknown": "Kan ikke oprette forbindelse til Tor-kontroller da indstillingerne ikke giver mening.",
|
||||||
@ -113,7 +98,6 @@
|
|||||||
"settings_error_unreadable_cookie_file": "Forbundet til Tor-kontroller, men kan ikke autentificere da din adgangskode kan være forkert, og din bruger ikke har tilladelse til at læse cookiefilen.",
|
"settings_error_unreadable_cookie_file": "Forbundet til Tor-kontroller, men kan ikke autentificere da din adgangskode kan være forkert, og din bruger ikke har tilladelse til at læse cookiefilen.",
|
||||||
"settings_error_bundled_tor_not_supported": "Bundet Tor understøttes ikke når der ikke bruges udviklertilstand i Windows eller MacOS.",
|
"settings_error_bundled_tor_not_supported": "Bundet Tor understøttes ikke når der ikke bruges udviklertilstand i Windows eller MacOS.",
|
||||||
"settings_error_bundled_tor_timeout": "Det tager for længe at oprette forbindelse til Tor. Din computer er måske offline, eller dit ur går forkert.",
|
"settings_error_bundled_tor_timeout": "Det tager for længe at oprette forbindelse til Tor. Din computer er måske offline, eller dit ur går forkert.",
|
||||||
"settings_error_bundled_tor_canceled": "Tor-processen lukkede inden den blev færdig med at oprette forbindelse.",
|
|
||||||
"settings_error_bundled_tor_broken": "Der er noget galt med OnionShare som opretter forbindelse til Tor i baggrunden:\n{}",
|
"settings_error_bundled_tor_broken": "Der er noget galt med OnionShare som opretter forbindelse til Tor i baggrunden:\n{}",
|
||||||
"settings_test_success": "Tillykke, OnionShare kan oprette forbindelse til Tor-kontrolleren.\n\nTor version: {}\nUnderstøtter kortvarige onion-tjenester: {}\nUnderstøtter usynlige onion-tjenester: {}",
|
"settings_test_success": "Tillykke, OnionShare kan oprette forbindelse til Tor-kontrolleren.\n\nTor version: {}\nUnderstøtter kortvarige onion-tjenester: {}\nUnderstøtter usynlige onion-tjenester: {}",
|
||||||
"error_tor_protocol_error": "Fejl under snak med Tor-kontrolleren.\nHvis du bruger Whonix, så tjek https://www.whonix.org/wiki/onionshare for at få OnionShare til at virke.",
|
"error_tor_protocol_error": "Fejl under snak med Tor-kontrolleren.\nHvis du bruger Whonix, så tjek https://www.whonix.org/wiki/onionshare for at få OnionShare til at virke.",
|
||||||
@ -131,6 +115,5 @@
|
|||||||
"gui_server_started_after_timeout": "Serveren startede efter dit valgte automatiske timeout.\nStart venligst en ny deling.",
|
"gui_server_started_after_timeout": "Serveren startede efter dit valgte automatiske timeout.\nStart venligst en ny deling.",
|
||||||
"gui_server_timeout_expired": "Den valgte timeout er allerede udløbet.\nOpdater venligst timeouten og herefter kan du starte deling.",
|
"gui_server_timeout_expired": "Den valgte timeout er allerede udløbet.\nOpdater venligst timeouten og herefter kan du starte deling.",
|
||||||
"share_via_onionshare": "Del via OnionShare",
|
"share_via_onionshare": "Del via OnionShare",
|
||||||
"gui_save_private_key_checkbox": "Brug en vedvarende URL\n(fravalg vil slette gemte URL)",
|
"gui_save_private_key_checkbox": "Brug en vedvarende URL\n(fravalg vil slette gemte URL)"
|
||||||
"persistent_url_in_use": "Denne deling bruger en vedvarende URL"
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,12 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Verbinde zum Tor-Kontrollport um den versteckten Dienst auf Port {0:d} laufen zu lassen.",
|
|
||||||
"cant_connect_ctrlport": "Konnte keine Verbindung zum Tor-Kontrollport auf Port {0:s} aufbauen. Läuft Tor?",
|
|
||||||
"cant_connect_socksport": "Konnte keine Verbindung zum Tor SOCKS5 Server auf Port {0:s} herstellen. OnionShare setzt voraus dass Tor Browser im Hintergrund läuft. Wenn du noch ihn noch noch nicht hast kannst du ihn unter https://www.torproject.org/ herunterladen.",
|
|
||||||
"preparing_files": "Dateien werden vorbereitet.",
|
"preparing_files": "Dateien werden vorbereitet.",
|
||||||
"wait_for_hs": "Warte auf HS:",
|
"wait_for_hs": "Warte auf HS:",
|
||||||
"wait_for_hs_trying": "Verbindungsversuch...",
|
|
||||||
"wait_for_hs_nope": "Noch nicht bereit.",
|
|
||||||
"wait_for_hs_yup": "Bereit!",
|
|
||||||
"give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:",
|
"give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:",
|
||||||
"ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten",
|
"ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten",
|
||||||
"not_a_file": "{0:s} ist keine Datei.",
|
"not_a_file": "{0:s} ist keine Datei.",
|
||||||
"download_page_loaded": "Seite geladen",
|
|
||||||
"other_page_loaded": "URL geladen",
|
"other_page_loaded": "URL geladen",
|
||||||
"closing_automatically": "Halte automatisch an, da der Download beendet wurde",
|
"closing_automatically": "Halte automatisch an, da der Download beendet wurde",
|
||||||
"large_filesize": "Warnung: Das Senden von großen Dateien kann Stunden dauern",
|
"large_filesize": "Warnung: Das Senden von großen Dateien kann Stunden dauern",
|
||||||
"error_tails_invalid_port": "Ungültiger Wert, Port muss eine ganze Zahl sein",
|
|
||||||
"error_tails_unknown_root": "Unbekannter Fehler mit Tails root Prozess",
|
|
||||||
"help_tails_port": "Nur für Tails: Port um den Firewall zu öffnen, starte onion service",
|
|
||||||
"help_local_only": "Nicht mit Tor benutzen, nur für Entwicklung",
|
"help_local_only": "Nicht mit Tor benutzen, nur für Entwicklung",
|
||||||
"help_stay_open": "Den onion service nicht anhalten nachdem ein Download beendet wurde",
|
"help_stay_open": "Den onion service nicht anhalten nachdem ein Download beendet wurde",
|
||||||
"help_debug": "Fehler auf Festplatte schreiben",
|
"help_debug": "Fehler auf Festplatte schreiben",
|
||||||
@ -25,8 +15,8 @@
|
|||||||
"gui_add": "Hinzufügen",
|
"gui_add": "Hinzufügen",
|
||||||
"gui_delete": "Löschen",
|
"gui_delete": "Löschen",
|
||||||
"gui_choose_items": "Auswählen",
|
"gui_choose_items": "Auswählen",
|
||||||
"gui_start_server": "Server starten",
|
"gui_share_start_server": "Server starten",
|
||||||
"gui_stop_server": "Server anhalten",
|
"gui_share_stop_server": "Server anhalten",
|
||||||
"gui_copy_url": "URL kopieren",
|
"gui_copy_url": "URL kopieren",
|
||||||
"gui_downloads": "Downloads:",
|
"gui_downloads": "Downloads:",
|
||||||
"gui_copied_url": "URL wurde in die Zwischenablage kopiert",
|
"gui_copied_url": "URL wurde in die Zwischenablage kopiert",
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
"config_onion_service": "Configuring onion service on port {0:d}.",
|
"config_onion_service": "Configuring onion service on port {0:d}.",
|
||||||
"preparing_files": "Preparing files to share.",
|
"preparing_files": "Preparing files to share.",
|
||||||
"wait_for_hs": "Waiting for HS to be ready:",
|
"wait_for_hs": "Waiting for HS to be ready:",
|
||||||
"wait_for_hs_trying": "Trying…",
|
|
||||||
"wait_for_hs_nope": "Not ready yet.",
|
|
||||||
"wait_for_hs_yup": "Ready!",
|
|
||||||
"give_this_url": "Give this address to the person you're sending the file to:",
|
"give_this_url": "Give this address to the person you're sending the file to:",
|
||||||
"give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
|
"give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
|
||||||
"give_this_url_receive": "Give this address to the people sending you files:",
|
"give_this_url_receive": "Give this address to the people sending you files:",
|
||||||
@ -14,14 +11,11 @@
|
|||||||
"not_a_readable_file": "{0:s} is not a readable file.",
|
"not_a_readable_file": "{0:s} is not a readable file.",
|
||||||
"no_filenames": "You must specify a list of files to share.",
|
"no_filenames": "You must specify a list of files to share.",
|
||||||
"no_available_port": "Could not start the Onion service as there was no available port.",
|
"no_available_port": "Could not start the Onion service as there was no available port.",
|
||||||
"download_page_loaded": "Download page loaded",
|
|
||||||
"other_page_loaded": "Address loaded",
|
"other_page_loaded": "Address loaded",
|
||||||
"close_on_timeout": "Stopped because timer expired",
|
"close_on_timeout": "Stopped because timer expired",
|
||||||
"closing_automatically": "Stopped because download finished",
|
"closing_automatically": "Stopped because download finished",
|
||||||
"timeout_download_still_running": "Waiting for download to complete",
|
"timeout_download_still_running": "Waiting for download to complete",
|
||||||
"large_filesize": "Warning: Sending large files could take hours",
|
"large_filesize": "Warning: Sending large files could take hours",
|
||||||
"error_tails_invalid_port": "Invalid value, port must be a regular number",
|
|
||||||
"error_tails_unknown_root": "Unknown error with Tails root process",
|
|
||||||
"systray_menu_exit": "Quit",
|
"systray_menu_exit": "Quit",
|
||||||
"systray_download_started_title": "OnionShare Download Started",
|
"systray_download_started_title": "OnionShare Download Started",
|
||||||
"systray_download_started_message": "A user started downloading your files",
|
"systray_download_started_message": "A user started downloading your files",
|
||||||
@ -29,6 +23,8 @@
|
|||||||
"systray_download_completed_message": "The user finished downloading your files",
|
"systray_download_completed_message": "The user finished downloading your files",
|
||||||
"systray_download_canceled_title": "OnionShare Download Canceled",
|
"systray_download_canceled_title": "OnionShare Download Canceled",
|
||||||
"systray_download_canceled_message": "The user canceled the download",
|
"systray_download_canceled_message": "The user canceled the download",
|
||||||
|
"systray_upload_started_title": "OnionShare Upload Started",
|
||||||
|
"systray_upload_started_message": "A user started uploading files to your computer",
|
||||||
"help_local_only": "Do not attempt to use Tor: For development only",
|
"help_local_only": "Do not attempt to use Tor: For development only",
|
||||||
"help_stay_open": "Keep onion service running after download has finished",
|
"help_stay_open": "Keep onion service running after download has finished",
|
||||||
"help_shutdown_timeout": "Shut down the onion service after N seconds",
|
"help_shutdown_timeout": "Shut down the onion service after N seconds",
|
||||||
@ -41,10 +37,14 @@
|
|||||||
"gui_add": "Add",
|
"gui_add": "Add",
|
||||||
"gui_delete": "Delete",
|
"gui_delete": "Delete",
|
||||||
"gui_choose_items": "Choose",
|
"gui_choose_items": "Choose",
|
||||||
"gui_start_server": "Start Sharing",
|
"gui_share_start_server": "Start Sharing",
|
||||||
"gui_stop_server": "Stop Sharing",
|
"gui_share_stop_server": "Stop Sharing",
|
||||||
"gui_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
|
"gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
|
||||||
"gui_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}",
|
"gui_share_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}",
|
||||||
|
"gui_receive_start_server": "Start Receive Mode",
|
||||||
|
"gui_receive_stop_server": "Stop Receive Mode",
|
||||||
|
"gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)",
|
||||||
|
"gui_receive_stop_server_shutdown_timeout_tooltip": "Receive mode will expire automatically at {}",
|
||||||
"gui_copy_url": "Copy Address",
|
"gui_copy_url": "Copy Address",
|
||||||
"gui_copy_hidservauth": "Copy HidServAuth",
|
"gui_copy_hidservauth": "Copy HidServAuth",
|
||||||
"gui_downloads": "Download History",
|
"gui_downloads": "Download History",
|
||||||
@ -55,18 +55,15 @@
|
|||||||
"gui_copied_url": "The OnionShare address has been copied to clipboard",
|
"gui_copied_url": "The OnionShare address has been copied to clipboard",
|
||||||
"gui_copied_hidservauth_title": "Copied HidServAuth",
|
"gui_copied_hidservauth_title": "Copied HidServAuth",
|
||||||
"gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
|
"gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
|
||||||
"gui_starting_server1": "Starting Tor onion service…",
|
|
||||||
"gui_starting_server2": "Compressing files…",
|
|
||||||
"gui_please_wait": "Starting… Click to cancel",
|
"gui_please_wait": "Starting… Click to cancel",
|
||||||
"error_hs_dir_cannot_create": "Cannot create onion service dir {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "onion service dir {0:s} is not writable",
|
|
||||||
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
||||||
"gui_download_progress_complete": "%p%, Time Elapsed: {0:s}",
|
"gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}",
|
||||||
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
|
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
|
||||||
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
||||||
"version_string": "OnionShare {0:s} | https://onionshare.org/",
|
"version_string": "OnionShare {0:s} | https://onionshare.org/",
|
||||||
"gui_quit_title": "Transfer in Progress",
|
"gui_quit_title": "Transfer in Progress",
|
||||||
"gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
|
"gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
|
||||||
|
"gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?",
|
||||||
"gui_quit_warning_quit": "Quit",
|
"gui_quit_warning_quit": "Quit",
|
||||||
"gui_quit_warning_dont_quit": "Cancel",
|
"gui_quit_warning_dont_quit": "Cancel",
|
||||||
"error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
|
"error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
|
||||||
@ -74,18 +71,17 @@
|
|||||||
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
||||||
"error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
|
"error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
|
||||||
"gui_settings_window_title": "Settings",
|
"gui_settings_window_title": "Settings",
|
||||||
"gui_settings_stealth_label": "Stealth (advanced)",
|
"gui_settings_whats_this": "<a href='{0:s}'>what's this?</a>",
|
||||||
"gui_settings_stealth_option": "Create stealth onion services",
|
"gui_settings_stealth_option": "Create stealth onion services (legacy)",
|
||||||
"gui_settings_stealth_option_details": "This makes OnionShare more secure, but also more difficult for the recipient to connect to.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">More information</a>.",
|
|
||||||
"gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.",
|
"gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.",
|
||||||
"gui_settings_autoupdate_label": "Check for upgrades",
|
"gui_settings_autoupdate_label": "Check for upgrades",
|
||||||
"gui_settings_autoupdate_option": "Notify me when upgrades are available",
|
"gui_settings_autoupdate_option": "Notify me when upgrades are available",
|
||||||
"gui_settings_autoupdate_timestamp": "Last checked: {}",
|
"gui_settings_autoupdate_timestamp": "Last checked: {}",
|
||||||
"gui_settings_autoupdate_timestamp_never": "Never",
|
"gui_settings_autoupdate_timestamp_never": "Never",
|
||||||
"gui_settings_autoupdate_check_button": "Check For Upgrades",
|
"gui_settings_autoupdate_check_button": "Check For Upgrades",
|
||||||
"gui_settings_sharing_label": "Sharing options",
|
"gui_settings_general_label": "General settings",
|
||||||
|
"gui_settings_sharing_label": "Sharing settings",
|
||||||
"gui_settings_close_after_first_download_option": "Stop sharing after first download",
|
"gui_settings_close_after_first_download_option": "Stop sharing after first download",
|
||||||
"gui_settings_systray_notifications": "Show desktop notifications",
|
|
||||||
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
|
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
|
||||||
"gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare",
|
"gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare",
|
||||||
"gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser",
|
"gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser",
|
||||||
@ -95,12 +91,10 @@
|
|||||||
"gui_settings_control_port_label": "Control port",
|
"gui_settings_control_port_label": "Control port",
|
||||||
"gui_settings_socket_file_label": "Socket file",
|
"gui_settings_socket_file_label": "Socket file",
|
||||||
"gui_settings_socks_label": "SOCKS port",
|
"gui_settings_socks_label": "SOCKS port",
|
||||||
"gui_settings_authenticate_label": "Tor authentication options",
|
"gui_settings_authenticate_label": "Tor authentication settings",
|
||||||
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
|
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
|
||||||
"gui_settings_authenticate_password_option": "Password",
|
"gui_settings_authenticate_password_option": "Password",
|
||||||
"gui_settings_authenticate_cookie_option": "Cookie",
|
|
||||||
"gui_settings_password_label": "Password",
|
"gui_settings_password_label": "Password",
|
||||||
"gui_settings_cookie_label": "Cookie path",
|
|
||||||
"gui_settings_tor_bridges": "Tor Bridge support",
|
"gui_settings_tor_bridges": "Tor Bridge support",
|
||||||
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
|
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
|
||||||
"gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
|
"gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
|
||||||
@ -126,10 +120,10 @@
|
|||||||
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.",
|
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.",
|
||||||
"settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.",
|
"settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.",
|
||||||
"settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.",
|
"settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.",
|
||||||
"settings_error_bundled_tor_canceled": "The Tor process closed before it could finish connecting.",
|
|
||||||
"settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}",
|
"settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}",
|
||||||
"settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}",
|
"settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}",
|
||||||
"error_tor_protocol_error": "Could not communicate with the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.",
|
"error_tor_protocol_error": "Could not communicate with the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.",
|
||||||
|
"error_invalid_private_key": "This private key type is unsupported",
|
||||||
"connecting_to_tor": "Connecting to the Tor network",
|
"connecting_to_tor": "Connecting to the Tor network",
|
||||||
"update_available": "A new version of OnionShare is available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}",
|
"update_available": "A new version of OnionShare is available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}",
|
||||||
"update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.",
|
"update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.",
|
||||||
@ -144,22 +138,50 @@
|
|||||||
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
|
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
|
||||||
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
|
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
|
||||||
"share_via_onionshare": "Share via OnionShare",
|
"share_via_onionshare": "Share via OnionShare",
|
||||||
"gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved addresses)",
|
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
|
||||||
"gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
|
"gui_save_private_key_checkbox": "Use a persistent address (legacy)",
|
||||||
|
"gui_share_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
|
||||||
|
"gui_receive_url_description": "<b>Anyone</b> with this link can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />",
|
||||||
"gui_url_label_persistent": "This share will not expire automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in Settings)",
|
"gui_url_label_persistent": "This share will not expire automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in Settings)",
|
||||||
"gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.",
|
"gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.",
|
||||||
"gui_url_label_onetime": "This share will expire after the first download",
|
"gui_url_label_onetime": "This share will expire after the first download",
|
||||||
"gui_url_label_onetime_and_persistent": "This share will expire after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
|
"gui_url_label_onetime_and_persistent": "This share will expire after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
|
||||||
"gui_status_indicator_stopped": "Ready to Share",
|
"gui_status_indicator_share_stopped": "Ready to Share",
|
||||||
"gui_status_indicator_working": "Starting…",
|
"gui_status_indicator_share_working": "Starting…",
|
||||||
"gui_status_indicator_started": "Sharing",
|
"gui_status_indicator_share_started": "Sharing",
|
||||||
|
"gui_status_indicator_receive_stopped": "Ready to Receive",
|
||||||
|
"gui_status_indicator_receive_working": "Starting…",
|
||||||
|
"gui_status_indicator_receive_started": "Receiving",
|
||||||
"gui_file_info": "{} Files, {}",
|
"gui_file_info": "{} Files, {}",
|
||||||
"gui_file_info_single": "{} File, {}",
|
"gui_file_info_single": "{} File, {}",
|
||||||
"info_in_progress_downloads_tooltip": "{} download(s) in progress",
|
"info_in_progress_downloads_tooltip": "{} download(s) in progress",
|
||||||
"info_completed_downloads_tooltip": "{} download(s) completed",
|
"info_completed_downloads_tooltip": "{} download(s) completed",
|
||||||
|
"info_in_progress_uploads_tooltip": "{} upload(s) in progress",
|
||||||
|
"info_completed_uploads_tooltip": "{} upload(s) completed",
|
||||||
"error_cannot_create_downloads_dir": "Error creating downloads folder: {}",
|
"error_cannot_create_downloads_dir": "Error creating downloads folder: {}",
|
||||||
"error_downloads_dir_not_writable": "The downloads folder isn't writable: {}",
|
"error_downloads_dir_not_writable": "The downloads folder isn't writable: {}",
|
||||||
"receive_mode_downloads_dir": "Files people send you will appear in this folder: {}",
|
"receive_mode_downloads_dir": "Files people send you will appear in this folder: {}",
|
||||||
"receive_mode_warning": "Warning: Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.",
|
"receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.",
|
||||||
"receive_mode_received_file": "Received file: {}"
|
"gui_receive_mode_warning": "<b>Some files can hack your computer if you open them!</b><br>Only open files from people you trust, or if you know what you're doing.",
|
||||||
|
"receive_mode_upload_starting": "Upload of total size {} is starting",
|
||||||
|
"receive_mode_received_file": "Received file: {}",
|
||||||
|
"gui_mode_share_button": "Share Files",
|
||||||
|
"gui_mode_receive_button": "Receive Files",
|
||||||
|
"gui_settings_receiving_label": "Receiving settings",
|
||||||
|
"gui_settings_downloads_label": "Save files to",
|
||||||
|
"gui_settings_downloads_button": "Browse",
|
||||||
|
"gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender",
|
||||||
|
"gui_settings_public_mode_checkbox": "Public mode",
|
||||||
|
"systray_close_server_title": "OnionShare Server Closed",
|
||||||
|
"systray_close_server_message": "A user closed the server",
|
||||||
|
"systray_page_loaded_title": "OnionShare Page Loaded",
|
||||||
|
"systray_download_page_loaded_message": "A user loaded the download page",
|
||||||
|
"systray_upload_page_loaded_message": "A user loaded the upload page",
|
||||||
|
"gui_uploads": "Upload History",
|
||||||
|
"gui_uploads_window_tooltip": "Show/hide uploads",
|
||||||
|
"gui_no_uploads": "No uploads yet.",
|
||||||
|
"gui_upload_in_progress": "Upload Started {}",
|
||||||
|
"gui_upload_finished_range": "Uploaded {} to {}",
|
||||||
|
"gui_upload_finished": "Uploaded {}",
|
||||||
|
"gui_open_folder_error_nautilus": "Cannot open folder the because nautilus is not available. You can find this file here: {}"
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,15 @@
|
|||||||
"config_onion_service": "Agordas onion service je pordo {0:d}.",
|
"config_onion_service": "Agordas onion service je pordo {0:d}.",
|
||||||
"preparing_files": "Preparas dosierojn por kundivido.",
|
"preparing_files": "Preparas dosierojn por kundivido.",
|
||||||
"wait_for_hs": "Atendas al hidden sevice por esti preta:",
|
"wait_for_hs": "Atendas al hidden sevice por esti preta:",
|
||||||
"wait_for_hs_trying": "Provas...",
|
|
||||||
"wait_for_hs_nope": "Ankoraŭ ne preta.",
|
|
||||||
"wait_for_hs_yup": "Preta!",
|
|
||||||
"give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:",
|
"give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:",
|
||||||
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
|
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
|
||||||
"ctrlc_to_stop": "Presu Ctrl-C por halti la servilon",
|
"ctrlc_to_stop": "Presu Ctrl-C por halti la servilon",
|
||||||
"not_a_file": "{0:s} ne estas dosiero.",
|
"not_a_file": "{0:s} ne estas dosiero.",
|
||||||
"download_page_loaded": "Download page loaded",
|
|
||||||
"other_page_loaded": "URL loaded",
|
"other_page_loaded": "URL loaded",
|
||||||
"closing_automatically": "Haltas aŭtomate ĉar la elŝuto finiĝis",
|
"closing_automatically": "Haltas aŭtomate ĉar la elŝuto finiĝis",
|
||||||
"large_filesize": "Atentigo: Sendado de grandaj dosieroj povas daŭri horojn",
|
"large_filesize": "Atentigo: Sendado de grandaj dosieroj povas daŭri horojn",
|
||||||
"error_tails_invalid_port": "Malĝusta valoro, pordo-numero devas esti plena numero",
|
|
||||||
"error_tails_unknown_root": "Nekonata eraro kun Tails-root-procezo",
|
|
||||||
"help_local_only": "Ne strebu uzi tor: nur por evoluado",
|
"help_local_only": "Ne strebu uzi tor: nur por evoluado",
|
||||||
"help_stay_open": "Lasu onion service funkcii post fino de elŝuto",
|
"help_stay_open": "Lasu onion service funkcii post fino de elŝuto",
|
||||||
"help_transparent_torification": "My system is transparently torified",
|
|
||||||
"help_stealth": "Create stealth onion service (advanced)",
|
"help_stealth": "Create stealth onion service (advanced)",
|
||||||
"help_debug": "Protokoli erarojn sur disko",
|
"help_debug": "Protokoli erarojn sur disko",
|
||||||
"help_filename": "Listo de dosieroj aŭ dosierujoj por kundividi",
|
"help_filename": "Listo de dosieroj aŭ dosierujoj por kundividi",
|
||||||
@ -25,34 +18,27 @@
|
|||||||
"gui_add": "Aldoni",
|
"gui_add": "Aldoni",
|
||||||
"gui_delete": "Forviŝi",
|
"gui_delete": "Forviŝi",
|
||||||
"gui_choose_items": "Elekti",
|
"gui_choose_items": "Elekti",
|
||||||
"gui_start_server": "Komenci kundividon",
|
"gui_share_start_server": "Komenci kundividon",
|
||||||
"gui_stop_server": "Ĉesigi kundividon",
|
"gui_share_stop_server": "Ĉesigi kundividon",
|
||||||
"gui_copy_url": "Kopii URL",
|
"gui_copy_url": "Kopii URL",
|
||||||
"gui_copy_hidservauth": "Kopii HidServAuth",
|
"gui_copy_hidservauth": "Kopii HidServAuth",
|
||||||
"gui_downloads": "Elŝutoj:",
|
"gui_downloads": "Elŝutoj:",
|
||||||
"gui_canceled": "Nuligita",
|
"gui_canceled": "Nuligita",
|
||||||
"gui_copied_url": "URL kopiita en tondujon",
|
"gui_copied_url": "URL kopiita en tondujon",
|
||||||
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
|
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
|
||||||
"gui_starting_server1": "Startigas Tor onion service...",
|
|
||||||
"gui_starting_server2": "Compressing files...",
|
|
||||||
"gui_please_wait": "Bonvolu atendi...",
|
"gui_please_wait": "Bonvolu atendi...",
|
||||||
"error_hs_dir_cannot_create": "Ne eblas krei hidden-service-dosierujon {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "ne eblas konservi dosierojn en hidden-service-dosierujo {0:s}",
|
|
||||||
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
|
||||||
"gui_download_progress_complete": "%p%, Tempo pasinta: {0:s}",
|
"gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}",
|
||||||
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
|
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
|
||||||
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
||||||
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
||||||
"gui_quit_warning": "Ĉu vi certas ke vi volas foriri?\nLa URL, kiun vi kundividas ne plu ekzistos.",
|
"gui_share_quit_warning": "Ĉu vi certas ke vi volas foriri?\nLa URL, kiun vi kundividas ne plu ekzistos.",
|
||||||
"gui_quit_warning_quit": "Foriri",
|
"gui_quit_warning_quit": "Foriri",
|
||||||
"gui_quit_warning_dont_quit": "Ne foriri",
|
"gui_quit_warning_dont_quit": "Ne foriri",
|
||||||
"error_rate_limit": "Iu atankanto povas provi diveni vian URL. Por eviti tion, OnionShare aŭtomate haltis la servilon. Por kundividi la dosierojn vi devas starti ĝin denove kaj kundividi la novan URL.",
|
"error_rate_limit": "Iu atankanto povas provi diveni vian URL. Por eviti tion, OnionShare aŭtomate haltis la servilon. Por kundividi la dosierojn vi devas starti ĝin denove kaj kundividi la novan URL.",
|
||||||
"zip_progress_bar_format": "Compressing files: %p%",
|
"zip_progress_bar_format": "Compressing files: %p%",
|
||||||
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
||||||
"error_ephemeral_not_supported": "OnionShare postulas almenaŭ Tor 0.2.7.1 kaj almenaŭ python3-stem 1.4.0.",
|
"error_ephemeral_not_supported": "OnionShare postulas almenaŭ Tor 0.2.7.1 kaj almenaŭ python3-stem 1.4.0.",
|
||||||
"gui_menu_file_menu": "&File",
|
|
||||||
"gui_menu_settings_action": "&Settings",
|
|
||||||
"gui_menu_quit_action": "&Quit",
|
|
||||||
"gui_settings_window_title": "Settings",
|
"gui_settings_window_title": "Settings",
|
||||||
"gui_settings_connection_type_label": "Kiel OnionShare devus konektiĝi al Tor?",
|
"gui_settings_connection_type_label": "Kiel OnionShare devus konektiĝi al Tor?",
|
||||||
"gui_settings_connection_type_automatic_option": "Provi aŭtomate agordi kun Tor Browser",
|
"gui_settings_connection_type_automatic_option": "Provi aŭtomate agordi kun Tor Browser",
|
||||||
@ -63,10 +49,7 @@
|
|||||||
"gui_settings_authenticate_label": "Tor authentication options",
|
"gui_settings_authenticate_label": "Tor authentication options",
|
||||||
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
|
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
|
||||||
"gui_settings_authenticate_password_option": "Pasvorto",
|
"gui_settings_authenticate_password_option": "Pasvorto",
|
||||||
"gui_settings_authenticate_cookie_option": "Kuketo",
|
|
||||||
"gui_settings_password_label": "Pasvorto",
|
"gui_settings_password_label": "Pasvorto",
|
||||||
"gui_settings_cookie_label": "Cookie path",
|
|
||||||
"gui_settings_button_test": "Test Settings",
|
|
||||||
"gui_settings_button_save": "Konservi",
|
"gui_settings_button_save": "Konservi",
|
||||||
"gui_settings_button_cancel": "Nuligi",
|
"gui_settings_button_cancel": "Nuligi",
|
||||||
"settings_saved": "Agordoj konservitaj en {}",
|
"settings_saved": "Agordoj konservitaj en {}",
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Conectando a puerto control de Tor para configurar servicio oculto en puerto {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "No se pudo conectar a puerto control de Tor en puertos {0:s}. ¿Está funcionando Tor?",
|
|
||||||
"cant_connect_socksport": "No se pudo conectar al servidor SOCKS5 de Tor en el puerto {0:s}. ¿Está funcionando Tor?",
|
|
||||||
"preparing_files": "Preparando los archivos para compartir.",
|
"preparing_files": "Preparando los archivos para compartir.",
|
||||||
"wait_for_hs": "Esperando a que HS esté listo:",
|
"wait_for_hs": "Esperando a que HS esté listo:",
|
||||||
"wait_for_hs_trying": "Probando...",
|
|
||||||
"wait_for_hs_nope": "No está listo todavía.",
|
|
||||||
"wait_for_hs_yup": "Listo!",
|
|
||||||
"give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:",
|
"give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:",
|
||||||
"ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor",
|
"ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor",
|
||||||
"not_a_file": "{0:s} no es un archivo.",
|
"not_a_file": "{0:s} no es un archivo.",
|
||||||
"download_page_loaded": "La página de descarga está lista.",
|
|
||||||
"other_page_loaded": "La URL está lista.",
|
"other_page_loaded": "La URL está lista.",
|
||||||
"closing_automatically": "Apagando automáticamente porque la descarga finalizó",
|
"closing_automatically": "Apagando automáticamente porque la descarga finalizó",
|
||||||
"error_tails_invalid_port": "Valor inválido, el puerto debe ser un entero",
|
|
||||||
"error_tails_unknown_root": "Error desconocido en el proceso de Tails ejecutando como roo",
|
|
||||||
"help_tails_port": "Sólo Tails: puerto para abrir en el firewall, al levantar el servicio oculto",
|
|
||||||
"help_local_only": "No intentar usar Tor: sólo para desarrollo",
|
"help_local_only": "No intentar usar Tor: sólo para desarrollo",
|
||||||
"help_stay_open": "Mantener el servicio oculto ejecutando después de que la descarga haya finalizado",
|
"help_stay_open": "Mantener el servicio oculto ejecutando después de que la descarga haya finalizado",
|
||||||
"help_debug": "Guardar registro de errores en el disco",
|
"help_debug": "Guardar registro de errores en el disco",
|
||||||
@ -24,8 +14,8 @@
|
|||||||
"gui_add": "Añadir",
|
"gui_add": "Añadir",
|
||||||
"gui_delete": "Eliminar",
|
"gui_delete": "Eliminar",
|
||||||
"gui_choose_items": "Elegir",
|
"gui_choose_items": "Elegir",
|
||||||
"gui_start_server": "Encender el Servidor",
|
"gui_share_start_server": "Encender el Servidor",
|
||||||
"gui_stop_server": "Detener el Servidor",
|
"gui_share_stop_server": "Detener el Servidor",
|
||||||
"gui_copy_url": "Copiar URL",
|
"gui_copy_url": "Copiar URL",
|
||||||
"gui_downloads": "Descargas:",
|
"gui_downloads": "Descargas:",
|
||||||
"gui_copied_url": "Se copió la URL en el portapapeles"
|
"gui_copied_url": "Se copió la URL en el portapapeles"
|
||||||
|
@ -1,43 +1,27 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Yhdistetään Torin ohjausporttiin että saadaan salattu palvelin porttiin {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Ei voi yhdistää Torin ohjausporttiin portissa {0:s}. OnionShare tarvitsee Tor Browserin toimimaan taustalla. Jos sinulla ei ole sitä niin voit hakea sen osoitteesta https://www.torproject.org/.",
|
|
||||||
"cant_connect_socksport": "Ei voi yhdistää Tor SOCKS5 palveluun portissa {0:s}. OnionShare tarvitsee Tor Browserin toimimaan taustalla. Jos sinulla ei ole sitä niin voit hakea sen osoitteesta https://www.torproject.org/.",
|
|
||||||
"preparing_files": "Valmistellaan tiedostoja jaettavaksi.",
|
"preparing_files": "Valmistellaan tiedostoja jaettavaksi.",
|
||||||
"wait_for_hs": "Odotetaan piilopalvelun valmistumista:",
|
"wait_for_hs": "Odotetaan piilopalvelun valmistumista:",
|
||||||
"wait_for_hs_trying": "Yritetään...",
|
|
||||||
"wait_for_hs_nope": "Ei vielä valmis.",
|
|
||||||
"wait_for_hs_yup": "Valmis!",
|
|
||||||
"give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:",
|
"give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:",
|
||||||
"ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen",
|
"ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen",
|
||||||
"not_a_file": "{0:s} Ei ole tiedosto.",
|
"not_a_file": "{0:s} Ei ole tiedosto.",
|
||||||
"download_page_loaded": "Lataussivu ladattu",
|
|
||||||
"other_page_loaded": "URL-osoite ladattu",
|
"other_page_loaded": "URL-osoite ladattu",
|
||||||
"closing_automatically": "Lataus valmis. Suljetaan automaattisesti",
|
"closing_automatically": "Lataus valmis. Suljetaan automaattisesti",
|
||||||
"large_filesize": "Varoitus: Isojen tiedostojen lähetys saattaa kestää tunteja",
|
"large_filesize": "Varoitus: Isojen tiedostojen lähetys saattaa kestää tunteja",
|
||||||
"error_tails_invalid_port": "Väärä arvo, portti pitää olla koknaisluku",
|
|
||||||
"error_tails_unknown_root": "Tuntematon virhe Tailsissa",
|
|
||||||
"help_tails_port": "Vain Tails: portti palomuurin läpi, käynnistetään salainen palvelin",
|
|
||||||
"help_local_only": "Älä käytä Toria: vain ohjelmakehitykseen",
|
"help_local_only": "Älä käytä Toria: vain ohjelmakehitykseen",
|
||||||
"help_stay_open": "Pidä piilopalvelu käynnissä latauksen jälkeen.",
|
"help_stay_open": "Pidä piilopalvelu käynnissä latauksen jälkeen.",
|
||||||
"help_transparent_torification": "Järjestelmäni käyttää Toria läpinäkyvästi",
|
|
||||||
"help_debug": "Tallentaa virheet levylle",
|
"help_debug": "Tallentaa virheet levylle",
|
||||||
"help_filename": "Luettele jaettavat tiedostot tai kansiot",
|
"help_filename": "Luettele jaettavat tiedostot tai kansiot",
|
||||||
"gui_drag_and_drop": "Vedä ja pudota\ntiedostot tänne",
|
"gui_drag_and_drop": "Vedä ja pudota\ntiedostot tänne",
|
||||||
"gui_add": "Lisää",
|
"gui_add": "Lisää",
|
||||||
"gui_delete": "Poista",
|
"gui_delete": "Poista",
|
||||||
"gui_choose_items": "Valitse",
|
"gui_choose_items": "Valitse",
|
||||||
"gui_start_server": "Käynnistä palvelin",
|
"gui_share_start_server": "Käynnistä palvelin",
|
||||||
"gui_stop_server": "Pysäytä palvelin",
|
"gui_share_stop_server": "Pysäytä palvelin",
|
||||||
"gui_copy_url": "Kopioi URL-osoite",
|
"gui_copy_url": "Kopioi URL-osoite",
|
||||||
"gui_downloads": "Lataukset:",
|
"gui_downloads": "Lataukset:",
|
||||||
"gui_canceled": "Peruutettu",
|
"gui_canceled": "Peruutettu",
|
||||||
"gui_copied_url": "URL-osoite kopioitu leikepöydälle",
|
"gui_copied_url": "URL-osoite kopioitu leikepöydälle",
|
||||||
"gui_starting_server1": "Käynnistetään Tor piilopalvelu...",
|
|
||||||
"gui_starting_server2": "Tiivistän tiedostoja...",
|
|
||||||
"gui_starting_server3": "Odotetaan Tor piilopalvelua...",
|
|
||||||
"gui_please_wait": "Odota...",
|
"gui_please_wait": "Odota...",
|
||||||
"error_hs_dir_cannot_create": "Piilopalvelulle ei pystytty luomaan hakemistoa {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "Piilopalvelun hakemistoon {0:s} ei voi kirjoittaa",
|
|
||||||
"using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua",
|
"using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua",
|
||||||
"zip_progress_bar_format": "Tiivistän tiedostoja: %p%"
|
"zip_progress_bar_format": "Tiivistän tiedostoja: %p%"
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,17 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Connexion au réseau Tor pour mettre en place un onion service sur le port {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Impossible de se connecter au port de contrôle Tor sur le port {0:s}. Est-ce que Tor tourne ?",
|
|
||||||
"preparing_files": "Préparation des fichiers à partager.",
|
"preparing_files": "Préparation des fichiers à partager.",
|
||||||
"wait_for_hs": "En attente du HS:",
|
"wait_for_hs": "En attente du HS:",
|
||||||
"wait_for_hs_trying": "Tentative...",
|
|
||||||
"wait_for_hs_nope": "Pas encore prêt.",
|
|
||||||
"wait_for_hs_yup": "Prêt !",
|
|
||||||
"give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :",
|
"give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :",
|
||||||
"ctrlc_to_stop": "Ctrl-C arrête le serveur",
|
"ctrlc_to_stop": "Ctrl-C arrête le serveur",
|
||||||
"not_a_file": "{0:s} n'est pas un fichier.",
|
"not_a_file": "{0:s} n'est pas un fichier.",
|
||||||
"download_page_loaded": "Page de téléchargement chargée",
|
|
||||||
"other_page_loaded": "URL chargée",
|
"other_page_loaded": "URL chargée",
|
||||||
"closing_automatically": "Fermeture automatique car le téléchargement est fini",
|
"closing_automatically": "Fermeture automatique car le téléchargement est fini",
|
||||||
"error_tails_invalid_port": "Valeur invalide, le port doit être un nombre entier",
|
|
||||||
"error_tails_unknown_root": "Erreur inconnue avec un processus root sur Tails",
|
|
||||||
"systray_menu_exit": "Quitter",
|
"systray_menu_exit": "Quitter",
|
||||||
"systray_download_started_title": "Téléchargement OnionShare Démarré",
|
"systray_download_started_title": "Téléchargement OnionShare Démarré",
|
||||||
"systray_download_started_message": "Un utilisateur télécharge vos fichiers",
|
"systray_download_started_message": "Un utilisateur télécharge vos fichiers",
|
||||||
"systray_download_completed_title": "Téléchargement OnionShare Complete",
|
"systray_download_completed_title": "Téléchargement OnionShare Complete",
|
||||||
"systray_download_canceled_title": "Téléchargement OnionShare Annulé",
|
"systray_download_canceled_title": "Téléchargement OnionShare Annulé",
|
||||||
"systray_download_canceled_message": "L'utilisateur a annulé le téléchargement",
|
"systray_download_canceled_message": "L'utilisateur a annulé le téléchargement",
|
||||||
"help_tails_port": "Seulement sur Tails: port pour ouvrir le firewall, démarrage du onion service",
|
|
||||||
"help_local_only": "Ne tentez pas d'utiliser Tor, uniquement pour développement",
|
"help_local_only": "Ne tentez pas d'utiliser Tor, uniquement pour développement",
|
||||||
"help_stay_open": "Laisser tourner le onion service après que le téléchargment soit fini",
|
"help_stay_open": "Laisser tourner le onion service après que le téléchargment soit fini",
|
||||||
"help_debug": "Enregistrer les erreurs sur le disque",
|
"help_debug": "Enregistrer les erreurs sur le disque",
|
||||||
@ -29,8 +20,8 @@
|
|||||||
"gui_add": "Ajouter",
|
"gui_add": "Ajouter",
|
||||||
"gui_delete": "Supprimer",
|
"gui_delete": "Supprimer",
|
||||||
"gui_choose_items": "Sélectionnez",
|
"gui_choose_items": "Sélectionnez",
|
||||||
"gui_start_server": "Démarrer le serveur",
|
"gui_share_start_server": "Démarrer le serveur",
|
||||||
"gui_stop_server": "Arrêter le serveur",
|
"gui_share_stop_server": "Arrêter le serveur",
|
||||||
"gui_copy_url": "Copier URL",
|
"gui_copy_url": "Copier URL",
|
||||||
"gui_copy_hidservauth": "Copier HidServAuth",
|
"gui_copy_hidservauth": "Copier HidServAuth",
|
||||||
"gui_downloads": "Téléchargements :",
|
"gui_downloads": "Téléchargements :",
|
||||||
|
@ -1,43 +1,27 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Connessione alla porta di controllo di Tor per inizializzare il servizio nascosto sulla porta {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Impossibile connettere alla porta di controllo di Tor sulla porta {0:s}. OnionShare richiede l'esecuzione in background di Tor Browser per funzionare. Se non è installato puoi scaricarlo da https://www.torproject.org/.",
|
|
||||||
"cant_connect_socksport": "Impossibile connettersi al server Tor SOCKS5 sulla porta {0:s}. OnionShare richiede l'esecuzione in background di Tor Browser per funzionare. Se non è installato puoi scaricarlo da https://www.torproject.org/.",
|
|
||||||
"preparing_files": "Preparazione dei files da condividere.",
|
"preparing_files": "Preparazione dei files da condividere.",
|
||||||
"wait_for_hs": "In attesa che l'HS sia pronto:",
|
"wait_for_hs": "In attesa che l'HS sia pronto:",
|
||||||
"wait_for_hs_trying": "Tentativo...",
|
|
||||||
"wait_for_hs_nope": "Non è ancora pronto.",
|
|
||||||
"wait_for_hs_yup": "Pronto!",
|
|
||||||
"give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:",
|
"give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:",
|
||||||
"ctrlc_to_stop": "Premi Ctrl-C per fermare il server",
|
"ctrlc_to_stop": "Premi Ctrl-C per fermare il server",
|
||||||
"not_a_file": "{0:s} non è un file.",
|
"not_a_file": "{0:s} non è un file.",
|
||||||
"download_page_loaded": "Pagina di Download caricata",
|
|
||||||
"other_page_loaded": "URL caricato",
|
"other_page_loaded": "URL caricato",
|
||||||
"closing_automatically": "Chiusura automatica dopo aver finito il download",
|
"closing_automatically": "Chiusura automatica dopo aver finito il download",
|
||||||
"large_filesize": "Attenzione: Inviare file di grandi dimensioni può richiedere ore",
|
"large_filesize": "Attenzione: Inviare file di grandi dimensioni può richiedere ore",
|
||||||
"error_tails_invalid_port": "Valore non valido, la porta deve essere un numero intero",
|
|
||||||
"error_tails_unknown_root": "Errore sconosciuto con il processo Tails root",
|
|
||||||
"help_tails_port": "Solo per Tails: porta per passare il firewall, avvio del servizio nascosto",
|
|
||||||
"help_local_only": "Non usare tor: è solo per lo sviluppo",
|
"help_local_only": "Non usare tor: è solo per lo sviluppo",
|
||||||
"help_stay_open": "Mantieni il servizio nascosto avviato anche dopo aver finito il download",
|
"help_stay_open": "Mantieni il servizio nascosto avviato anche dopo aver finito il download",
|
||||||
"help_transparent_torification": "Il mio sistema usa tor in modo trasparente",
|
|
||||||
"help_debug": "Registra gli errori sul disco",
|
"help_debug": "Registra gli errori sul disco",
|
||||||
"help_filename": "Lista dei file o cartelle da condividere",
|
"help_filename": "Lista dei file o cartelle da condividere",
|
||||||
"gui_drag_and_drop": "Prendi e rilascia\ni file qui sopra",
|
"gui_drag_and_drop": "Prendi e rilascia\ni file qui sopra",
|
||||||
"gui_add": "Aggiungi",
|
"gui_add": "Aggiungi",
|
||||||
"gui_delete": "Cancella",
|
"gui_delete": "Cancella",
|
||||||
"gui_choose_items": "Scegli",
|
"gui_choose_items": "Scegli",
|
||||||
"gui_start_server": "Inizia la condivisione",
|
"gui_share_start_server": "Inizia la condivisione",
|
||||||
"gui_stop_server": "Ferma la condivisione",
|
"gui_share_stop_server": "Ferma la condivisione",
|
||||||
"gui_copy_url": "Copia lo URL",
|
"gui_copy_url": "Copia lo URL",
|
||||||
"gui_downloads": "Downloads:",
|
"gui_downloads": "Downloads:",
|
||||||
"gui_canceled": "Cancellati",
|
"gui_canceled": "Cancellati",
|
||||||
"gui_copied_url": "URL Copiato nella clipboard",
|
"gui_copied_url": "URL Copiato nella clipboard",
|
||||||
"gui_starting_server1": "Avviamento del servizio nascosto Tor...",
|
|
||||||
"gui_starting_server2": "Elaborazione files...",
|
|
||||||
"gui_starting_server3": "In attesa del servizio nascosto Tor...",
|
|
||||||
"gui_please_wait": "Attendere prego...",
|
"gui_please_wait": "Attendere prego...",
|
||||||
"error_hs_dir_cannot_create": "Impossibile create la cartella per il servizio nascosto {0:s}",
|
|
||||||
"error_hs_dir_not_writable": "La cartella per il servizio nascosto {0:s} non ha i permessi di scrittura",
|
|
||||||
"using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione",
|
"using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione",
|
||||||
"zip_progress_bar_format": "Elaborazione files: %p%"
|
"zip_progress_bar_format": "Elaborazione files: %p%"
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,17 @@
|
|||||||
"config_onion_service": "Onion service configureren op poort {0:d}.",
|
"config_onion_service": "Onion service configureren op poort {0:d}.",
|
||||||
"preparing_files": "Bestanden om te delen aan het voorbereiden.",
|
"preparing_files": "Bestanden om te delen aan het voorbereiden.",
|
||||||
"wait_for_hs": "Wachten op gereed zijn van HS:",
|
"wait_for_hs": "Wachten op gereed zijn van HS:",
|
||||||
"wait_for_hs_trying": "Proberen...",
|
|
||||||
"wait_for_hs_nope": "Nog niet gereed.",
|
|
||||||
"wait_for_hs_yup": "Gereed!",
|
|
||||||
"give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:",
|
"give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:",
|
||||||
"give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:",
|
"give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:",
|
||||||
"ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen",
|
"ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen",
|
||||||
"not_a_file": "{0:s} is geen bestand.",
|
"not_a_file": "{0:s} is geen bestand.",
|
||||||
"not_a_readable_file": "{0:s} is geen leesbaar bestand.",
|
"not_a_readable_file": "{0:s} is geen leesbaar bestand.",
|
||||||
"no_available_port": "Kan de Onion service niet starten, er zijn geen poorten beschikbaar.",
|
"no_available_port": "Kan de Onion service niet starten, er zijn geen poorten beschikbaar.",
|
||||||
"download_page_loaded": "Downloadpagina geladen",
|
|
||||||
"other_page_loaded": "URL geladen",
|
"other_page_loaded": "URL geladen",
|
||||||
"close_on_timeout": "Sluit automatisch omdat timeout bereikt is",
|
"close_on_timeout": "Sluit automatisch omdat timeout bereikt is",
|
||||||
"closing_automatically": "Sluit automatisch omdat download gereed is",
|
"closing_automatically": "Sluit automatisch omdat download gereed is",
|
||||||
"timeout_download_still_running": "Wachten totdat download gereed is voor automatisch sluiten",
|
"timeout_download_still_running": "Wachten totdat download gereed is voor automatisch sluiten",
|
||||||
"large_filesize": "Waarschuwing: Versturen van grote bestanden kan uren duren",
|
"large_filesize": "Waarschuwing: Versturen van grote bestanden kan uren duren",
|
||||||
"error_tails_invalid_port": "Ongeldige waarde, poort moet een integer zijn",
|
|
||||||
"error_tails_unknown_root": "Onbekende fout met het Tails root proces",
|
|
||||||
"systray_menu_exit": "Afsluiten",
|
"systray_menu_exit": "Afsluiten",
|
||||||
"systray_download_started_title": "OnionShare download gestart",
|
"systray_download_started_title": "OnionShare download gestart",
|
||||||
"systray_download_started_message": "Een gebruiker is begonnen met downloaden van je bestanden",
|
"systray_download_started_message": "Een gebruiker is begonnen met downloaden van je bestanden",
|
||||||
@ -29,7 +23,6 @@
|
|||||||
"help_local_only": "Maak geen gebruik van Tor, alleen voor ontwikkeling",
|
"help_local_only": "Maak geen gebruik van Tor, alleen voor ontwikkeling",
|
||||||
"help_stay_open": "Laat verborgen service draaien nadat download gereed is",
|
"help_stay_open": "Laat verborgen service draaien nadat download gereed is",
|
||||||
"help_shutdown_timeout": "Sluit de Onion service na N seconden",
|
"help_shutdown_timeout": "Sluit de Onion service na N seconden",
|
||||||
"help_transparent_torification": "Mijn systeem gebruikt Tor als proxyserver",
|
|
||||||
"help_stealth": "Maak stealth Onion service (geavanceerd)",
|
"help_stealth": "Maak stealth Onion service (geavanceerd)",
|
||||||
"help_debug": "Log fouten naar harde schijf",
|
"help_debug": "Log fouten naar harde schijf",
|
||||||
"help_filename": "Lijst van bestanden of mappen om te delen",
|
"help_filename": "Lijst van bestanden of mappen om te delen",
|
||||||
@ -38,25 +31,19 @@
|
|||||||
"gui_add": "Toevoegen",
|
"gui_add": "Toevoegen",
|
||||||
"gui_delete": "Verwijder",
|
"gui_delete": "Verwijder",
|
||||||
"gui_choose_items": "Kies",
|
"gui_choose_items": "Kies",
|
||||||
"gui_start_server": "Start server",
|
|
||||||
"gui_stop_server": "Stop server",
|
|
||||||
"gui_copy_url": "Kopieer URL",
|
"gui_copy_url": "Kopieer URL",
|
||||||
"gui_copy_hidservauth": "Kopieer HidServAuth",
|
"gui_copy_hidservauth": "Kopieer HidServAuth",
|
||||||
"gui_downloads": "Downloads:",
|
"gui_downloads": "Downloads:",
|
||||||
"gui_canceled": "Afgebroken",
|
"gui_canceled": "Afgebroken",
|
||||||
"gui_copied_url": "URL gekopieerd naar klembord",
|
"gui_copied_url": "URL gekopieerd naar klembord",
|
||||||
"gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord",
|
"gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord",
|
||||||
"gui_starting_server1": "Tor onion service wordt gestart...",
|
|
||||||
"gui_starting_server2": "Bestanden verwerken...",
|
|
||||||
"gui_please_wait": "Moment geduld...",
|
"gui_please_wait": "Moment geduld...",
|
||||||
"error_hs_dir_cannot_create": "Kan verborgen service map {0:s} niet aanmaken",
|
|
||||||
"error_hs_dir_not_writable": "Verborgen service map {0:s} is niet schrijfbaar",
|
|
||||||
"using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie",
|
"using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie",
|
||||||
"gui_download_progress_complete": "%p%, Tijd verstreken: {0:s}",
|
"gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}",
|
||||||
"gui_download_progress_starting": "{0:s}, %p% (ETA berekenen)",
|
"gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)",
|
||||||
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
|
||||||
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
"version_string": "Onionshare {0:s} | https://onionshare.org/",
|
||||||
"gui_quit_warning": "Weet je zeker dat je wilt afsluiten?\nDe URL die je aan het delen bent zal niet meer bestaan.",
|
"gui_share_quit_warning": "Weet je zeker dat je wilt afsluiten?\nDe URL die je aan het delen bent zal niet meer bestaan.",
|
||||||
"gui_quit_warning_quit": "Afsluiten",
|
"gui_quit_warning_quit": "Afsluiten",
|
||||||
"gui_quit_warning_dont_quit": "Niet afsluiten",
|
"gui_quit_warning_dont_quit": "Niet afsluiten",
|
||||||
"error_rate_limit": "Een aanvaller probeert misschien je URL te gokken. Om dit te voorkomen heeft OnionShare de server automatisch gestopt. Om de bestanden te delen moet je de server opnieuw starten en de nieuwe URL delen.",
|
"error_rate_limit": "Een aanvaller probeert misschien je URL te gokken. Om dit te voorkomen heeft OnionShare de server automatisch gestopt. Om de bestanden te delen moet je de server opnieuw starten en de nieuwe URL delen.",
|
||||||
@ -74,7 +61,6 @@
|
|||||||
"gui_settings_autoupdate_check_button": "Controleer voor update",
|
"gui_settings_autoupdate_check_button": "Controleer voor update",
|
||||||
"gui_settings_sharing_label": "Deel opties",
|
"gui_settings_sharing_label": "Deel opties",
|
||||||
"gui_settings_close_after_first_download_option": "Stop delen na eerste download",
|
"gui_settings_close_after_first_download_option": "Stop delen na eerste download",
|
||||||
"gui_settings_systray_notifications": "Laat desktop notificaties zien",
|
|
||||||
"gui_settings_connection_type_label": "Hoe moet OnionShare verbinden met Tor?",
|
"gui_settings_connection_type_label": "Hoe moet OnionShare verbinden met Tor?",
|
||||||
"gui_settings_connection_type_bundled_option": "Gebruik Tor die is meegeleverd met OnionShare",
|
"gui_settings_connection_type_bundled_option": "Gebruik Tor die is meegeleverd met OnionShare",
|
||||||
"gui_settings_connection_type_automatic_option": "Probeer automatische configuratie met Tor Browser",
|
"gui_settings_connection_type_automatic_option": "Probeer automatische configuratie met Tor Browser",
|
||||||
@ -87,13 +73,10 @@
|
|||||||
"gui_settings_authenticate_label": "Tor authenticatie opties",
|
"gui_settings_authenticate_label": "Tor authenticatie opties",
|
||||||
"gui_settings_authenticate_no_auth_option": "Geen authenticatie of cookie authenticatie",
|
"gui_settings_authenticate_no_auth_option": "Geen authenticatie of cookie authenticatie",
|
||||||
"gui_settings_authenticate_password_option": "Wachtwoord",
|
"gui_settings_authenticate_password_option": "Wachtwoord",
|
||||||
"gui_settings_authenticate_cookie_option": "Cookie",
|
|
||||||
"gui_settings_password_label": "Wachtwoord",
|
"gui_settings_password_label": "Wachtwoord",
|
||||||
"gui_settings_cookie_label": "Cookie pad",
|
|
||||||
"gui_settings_button_save": "Opslaan",
|
"gui_settings_button_save": "Opslaan",
|
||||||
"gui_settings_button_cancel": "Annuleren",
|
"gui_settings_button_cancel": "Annuleren",
|
||||||
"gui_settings_button_help": "Help",
|
"gui_settings_button_help": "Help",
|
||||||
"gui_settings_shutdown_timeout_choice": "Auto-stop timer instellen?",
|
|
||||||
"gui_settings_shutdown_timeout": "Stop delen om:",
|
"gui_settings_shutdown_timeout": "Stop delen om:",
|
||||||
"settings_saved": "Instellingen opgeslagen in {}",
|
"settings_saved": "Instellingen opgeslagen in {}",
|
||||||
"settings_error_unknown": "Kan geen verbinding maken met de Tor controller omdat de instellingen niet kloppen.",
|
"settings_error_unknown": "Kan geen verbinding maken met de Tor controller omdat de instellingen niet kloppen.",
|
||||||
@ -105,7 +88,6 @@
|
|||||||
"settings_error_unreadable_cookie_file": "Verbonden met Tor controller, maar kan niet authenticeren omdat wachtwoord onjuist is en gebruiker heeft niet de permissies om cookie bestand te lezen.",
|
"settings_error_unreadable_cookie_file": "Verbonden met Tor controller, maar kan niet authenticeren omdat wachtwoord onjuist is en gebruiker heeft niet de permissies om cookie bestand te lezen.",
|
||||||
"settings_error_bundled_tor_not_supported": "Meegeleverde Tor is niet onersteunt wanneer je niet de ontwikkelaarsmodus gebruikt in Windows or macOS.",
|
"settings_error_bundled_tor_not_supported": "Meegeleverde Tor is niet onersteunt wanneer je niet de ontwikkelaarsmodus gebruikt in Windows or macOS.",
|
||||||
"settings_error_bundled_tor_timeout": "Verbinden met Tor duurt te lang. Misschien is je computer offline, of je klok is niet accuraat.",
|
"settings_error_bundled_tor_timeout": "Verbinden met Tor duurt te lang. Misschien is je computer offline, of je klok is niet accuraat.",
|
||||||
"settings_error_bundled_tor_canceled": "Het Tor is afgesloten voordat het kon verbinden.",
|
|
||||||
"settings_error_bundled_tor_broken": "OnionShare kan niet verbinden met Tor op de achtergrond:\n{}",
|
"settings_error_bundled_tor_broken": "OnionShare kan niet verbinden met Tor op de achtergrond:\n{}",
|
||||||
"settings_test_success": "Gefeliciteerd, OnionShare kan verbinden met de Tor controller.\n\nTor version: {}\nOndersteunt kortstondige onion services: {}\nOndersteunt geheime onion services: {}",
|
"settings_test_success": "Gefeliciteerd, OnionShare kan verbinden met de Tor controller.\n\nTor version: {}\nOndersteunt kortstondige onion services: {}\nOndersteunt geheime onion services: {}",
|
||||||
"error_tor_protocol_error": "Fout bij praten met de Tor controller.\nAls je Whonix gebruikt, kijk dan hier https://www.whonix.org/wiki/onionshare om OnionShare werkend te krijgen.",
|
"error_tor_protocol_error": "Fout bij praten met de Tor controller.\nAls je Whonix gebruikt, kijk dan hier https://www.whonix.org/wiki/onionshare om OnionShare werkend te krijgen.",
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Kobler til Tors kontroll-port for å sette opp en gjemt tjeneste på port {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Klarte ikke å koble til Tors kontroll-porter {0:s}. Sjekk at Tor kjører.",
|
|
||||||
"give_this_url": "Gi personen du vil sende filen til denne URL-en:",
|
"give_this_url": "Gi personen du vil sende filen til denne URL-en:",
|
||||||
"ctrlc_to_stop": "Trykk Ctrl+C for å stoppe serveren.",
|
"ctrlc_to_stop": "Trykk Ctrl+C for å stoppe serveren.",
|
||||||
"not_a_file": "{0:s} er ikke en fil.",
|
"not_a_file": "{0:s} er ikke en fil.",
|
||||||
"gui_copied_url": "Kopierte URL-en til utklippstavlen",
|
"gui_copied_url": "Kopierte URL-en til utklippstavlen",
|
||||||
"download_page_loaded": "Nedlastingsside lastet",
|
|
||||||
"other_page_loaded": "En annen side har blitt lastet"
|
"other_page_loaded": "En annen side har blitt lastet"
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Conectando-se à porta de controle Tor para configurar serviço escondido na porta {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Não pode conectar à porta de controle Tor na porta {0:s}. O Tor está rodando?",
|
|
||||||
"give_this_url": "Passe este URL para a pessoa que deve receber o arquivo:",
|
"give_this_url": "Passe este URL para a pessoa que deve receber o arquivo:",
|
||||||
"ctrlc_to_stop": "Pressione Ctrl-C para parar o servidor",
|
"ctrlc_to_stop": "Pressione Ctrl-C para parar o servidor",
|
||||||
"not_a_file": "{0:s} não é um arquivo.",
|
"not_a_file": "{0:s} não é um arquivo.",
|
||||||
"gui_copied_url": "URL foi copiado para área de transferência",
|
"gui_copied_url": "URL foi copiado para área de transferência",
|
||||||
"download_page_loaded": "Página de download carregada",
|
|
||||||
"other_page_loaded": "Outra página tem sido carregada"
|
"other_page_loaded": "Outra página tem sido carregada"
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "Соединяемся с контрольным портом Tor для создания скрытого сервиса на порту {0:d}.",
|
|
||||||
"cant_connect_ctrlport": "Невозможно соединиться с контрольным портом Tor на порту {0:s}. Tor запущен?",
|
|
||||||
"give_this_url": "Отправьте эту ссылку тому человеку, которому вы хотите передать файл:",
|
"give_this_url": "Отправьте эту ссылку тому человеку, которому вы хотите передать файл:",
|
||||||
"ctrlc_to_stop": "Нажмите Ctrl-C чтобы остановить сервер",
|
"ctrlc_to_stop": "Нажмите Ctrl-C чтобы остановить сервер",
|
||||||
"not_a_file": "{0:s} не является файлом.",
|
"not_a_file": "{0:s} не является файлом.",
|
||||||
"gui_copied_url": "Ссылка скопирована в буфер обмена",
|
"gui_copied_url": "Ссылка скопирована в буфер обмена",
|
||||||
"download_page_loaded": "Страница закачки загружена",
|
|
||||||
"other_page_loaded": "Другая страница была загружена",
|
"other_page_loaded": "Другая страница была загружена",
|
||||||
"gui_copy_url": "Скопировать ссылку"
|
"gui_copy_url": "Скопировать ссылку"
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,27 @@
|
|||||||
{
|
{
|
||||||
"connecting_ctrlport": "{0:d} portundaki gizli hizmet(GH) kurulumu için Tor kontrol portuna bağlanıyor.",
|
|
||||||
"cant_connect_ctrlport": "Tor kontrol {0:s} portuna bağlanamıyor. OnionShare çalışması için arkaplanda Tor Browser çalışması gerekiyor. Tor Browser indirmediyseniz, https://www.torproject.org/",
|
|
||||||
"cant_connect_socksport": "Tor SOCKS5 sunucu {0:s} portuna bağlanamıyor. OnionShare çalışması için arkaplanda Tor Browser çalışması gerekiyor. Tor Browser indirmediyseniz, https://www.torproject.org/",
|
|
||||||
"preparing_files": "Paylaşmak için dosyalar hazırlanıyor.",
|
"preparing_files": "Paylaşmak için dosyalar hazırlanıyor.",
|
||||||
"wait_for_hs": "GH hazır olması bekleniyor:",
|
"wait_for_hs": "GH hazır olması bekleniyor:",
|
||||||
"wait_for_hs_trying": "Deneniyor...",
|
|
||||||
"wait_for_hs_nope": "Henüz hazır değil.",
|
|
||||||
"wait_for_hs_yup": "Hazır!",
|
|
||||||
"give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:",
|
"give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:",
|
||||||
"ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın",
|
"ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın",
|
||||||
"not_a_file": "{0:s} dosya değil.",
|
"not_a_file": "{0:s} dosya değil.",
|
||||||
"download_page_loaded": "İndirme sayfası yüklendi",
|
|
||||||
"other_page_loaded": "Diğer sayfa yüklendi",
|
"other_page_loaded": "Diğer sayfa yüklendi",
|
||||||
"closing_automatically": "İndirme işlemi tamamlandığı için kendiliğinden durduruluyor",
|
"closing_automatically": "İndirme işlemi tamamlandığı için kendiliğinden durduruluyor",
|
||||||
"large_filesize": "Uyarı: Büyük dosyaların gönderimi saatler sürebilir",
|
"large_filesize": "Uyarı: Büyük dosyaların gönderimi saatler sürebilir",
|
||||||
"error_tails_invalid_port": "Geçersiz değer, port sayı olmalıdır",
|
|
||||||
"error_tails_unknown_root": "Tails ana işlemi ile ilgili bilinmeyen hata",
|
|
||||||
"help_tails_port": "Sadece Tails: port for opening firewall, starting onion service",
|
|
||||||
"help_local_only": "Tor kullanmaya kalkışmayın: sadece geliştirme için",
|
"help_local_only": "Tor kullanmaya kalkışmayın: sadece geliştirme için",
|
||||||
"help_stay_open": "İndirme tamamlandıktan sonra gizli hizmeti çalıştırmaya devam et",
|
"help_stay_open": "İndirme tamamlandıktan sonra gizli hizmeti çalıştırmaya devam et",
|
||||||
"help_transparent_torification": "Sistemim apaçık torlu",
|
|
||||||
"help_debug": "Hata kayıtlarını diske kaydet",
|
"help_debug": "Hata kayıtlarını diske kaydet",
|
||||||
"help_filename": "Paylaşmak için dosya ve klasörler listesi",
|
"help_filename": "Paylaşmak için dosya ve klasörler listesi",
|
||||||
"gui_drag_and_drop": "Dosyaları buraya\n Sürükle ve Bırak",
|
"gui_drag_and_drop": "Dosyaları buraya\n Sürükle ve Bırak",
|
||||||
"gui_add": "Ekle",
|
"gui_add": "Ekle",
|
||||||
"gui_delete": "Sil",
|
"gui_delete": "Sil",
|
||||||
"gui_choose_items": "Seç",
|
"gui_choose_items": "Seç",
|
||||||
"gui_start_server": "Paylaşımı Başlat",
|
"gui_share_start_server": "Paylaşımı Başlat",
|
||||||
"gui_stop_server": "Paylaşımı Durdur",
|
"gui_share_stop_server": "Paylaşımı Durdur",
|
||||||
"gui_copy_url": "URL Kopyala",
|
"gui_copy_url": "URL Kopyala",
|
||||||
"gui_downloads": "İndirilenler:",
|
"gui_downloads": "İndirilenler:",
|
||||||
"gui_canceled": "İptal edilen",
|
"gui_canceled": "İptal edilen",
|
||||||
"gui_copied_url": "Panoya kopyalanan URL",
|
"gui_copied_url": "Panoya kopyalanan URL",
|
||||||
"gui_starting_server1": "Tor gizli hizmeti başlatılıyor...",
|
|
||||||
"gui_starting_server2": "Dosyalar hazırlanıyor...",
|
|
||||||
"gui_starting_server3": "Tor gizli hizmeti bekleniyor...",
|
|
||||||
"gui_please_wait": "Lütfen bekleyin...",
|
"gui_please_wait": "Lütfen bekleyin...",
|
||||||
"error_hs_dir_cannot_create": "Gizli hizmet klasörü {0:s} oluşturulamıyor",
|
|
||||||
"error_hs_dir_not_writable": "Gizle hizmet klasörü {0:s} yazılabilir değil",
|
|
||||||
"using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor",
|
"using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor",
|
||||||
"zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%"
|
"zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%"
|
||||||
}
|
}
|
||||||
|
@ -142,3 +142,53 @@ ul.flashes li {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.error {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #c90c0c;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.info {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #000000;
|
||||||
|
background-color: #a9e26c;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info img {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .info-header {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #666666;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .info-description {
|
||||||
|
color: #666666;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>OnionShare: Error 404</title>
|
<title>OnionShare: 404 Not Found</title>
|
||||||
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
|
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
|
||||||
|
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Error 404: You probably typed the OnionShare address wrong</p>
|
<div class="info-wrapper">
|
||||||
|
<div class="info">
|
||||||
|
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
|
||||||
|
<p class="info-header">404 Not Found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,8 +3,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>OnionShare is closed</title>
|
<title>OnionShare is closed</title>
|
||||||
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
|
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
|
||||||
|
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Thank you for using OnionShare</p>
|
<header class="clearfix">
|
||||||
|
<img class="logo" src="/static/img/logo.png" title="OnionShare">
|
||||||
|
<h1>OnionShare</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="info-wrapper">
|
||||||
|
<div class="info">
|
||||||
|
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
|
||||||
|
<p class="info-header">Thank you for using OnionShare</p>
|
||||||
|
<p class="info-description">You may now close this window.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -17,15 +17,15 @@
|
|||||||
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
|
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
|
||||||
<p class="upload-header">Send Files</p>
|
<p class="upload-header">Send Files</p>
|
||||||
<p class="upload-description">Select the files you want to send, then click "Send Files"...</p>
|
<p class="upload-description">Select the files you want to send, then click "Send Files"...</p>
|
||||||
<form method="post" enctype="multipart/form-data" action="/{{ slug }}/upload">
|
<form method="post" enctype="multipart/form-data" action="{{ upload_action }}">
|
||||||
<p><input type="file" name="file[]" multiple /></p>
|
<p><input type="file" name="file[]" multiple /></p>
|
||||||
<p><input type="submit" class="button" value="Send Files" /></p>
|
<p><input type="submit" class="button" value="Send Files" /></p>
|
||||||
<div>
|
<div>
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class=flashes>
|
<ul class=flashes>
|
||||||
{% for message in messages %}
|
{% for category, message in messages %}
|
||||||
<li>{{ message }}</li>
|
<li class="{{ category }}">{{ message }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -34,10 +34,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if receive_allow_receiver_shutdown %}
|
||||||
<form method="post" action="/{{ slug }}/close">
|
{% with messages = get_flashed_messages() %}
|
||||||
<input type="submit" class="close-button" value="I'm Finished Uploading" />
|
{% if messages %}
|
||||||
|
<form method="post" action="{{ close_action }}">
|
||||||
|
<input type="submit" class="close-button" value="I'm Finished Sending" />
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -13,7 +13,11 @@
|
|||||||
<div class="right">
|
<div class="right">
|
||||||
<ul>
|
<ul>
|
||||||
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
|
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
|
||||||
|
{% if slug %}
|
||||||
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
|
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a class="button" href='/download'>Download Files</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<img class="logo" src="/static/img/logo.png" title="OnionShare">
|
<img class="logo" src="/static/img/logo.png" title="OnionShare">
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
Package3: onionshare
|
Package3: onionshare
|
||||||
Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy
|
Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy
|
||||||
Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5
|
Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5
|
||||||
Suite: xenial
|
Suite: bionic
|
||||||
X-Python3-Version: >= 3.4
|
X-Python3-Version: >= 3.6
|
||||||
|
@ -8,7 +8,7 @@ import tempfile
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from onionshare import common, web
|
from onionshare import common, web, settings
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_dir_1024():
|
def temp_dir_1024():
|
||||||
@ -152,3 +152,9 @@ def time_strftime(monkeypatch):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def common_obj():
|
def common_obj():
|
||||||
return common.Common()
|
return common.Common()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def settings_obj(sys_onionshare_dev_mode, platform_linux):
|
||||||
|
_common = common.Common()
|
||||||
|
_common.version = 'DUMMY_VERSION_1.2.3'
|
||||||
|
return settings.Settings(_common)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -49,7 +49,6 @@ class TestOnionShare:
|
|||||||
assert onionshare_obj.stealth is None
|
assert onionshare_obj.stealth is None
|
||||||
assert onionshare_obj.cleanup_filenames == []
|
assert onionshare_obj.cleanup_filenames == []
|
||||||
assert onionshare_obj.local_only is False
|
assert onionshare_obj.local_only is False
|
||||||
assert onionshare_obj.stay_open is False
|
|
||||||
|
|
||||||
def test_set_stealth_true(self, onionshare_obj):
|
def test_set_stealth_true(self, onionshare_obj):
|
||||||
onionshare_obj.set_stealth(True)
|
onionshare_obj.set_stealth(True)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -51,7 +51,6 @@ class TestSettings:
|
|||||||
'auth_type': 'no_auth',
|
'auth_type': 'no_auth',
|
||||||
'auth_password': '',
|
'auth_password': '',
|
||||||
'close_after_first_download': True,
|
'close_after_first_download': True,
|
||||||
'systray_notifications': True,
|
|
||||||
'shutdown_timeout': False,
|
'shutdown_timeout': False,
|
||||||
'use_stealth': False,
|
'use_stealth': False,
|
||||||
'use_autoupdate': True,
|
'use_autoupdate': True,
|
||||||
@ -60,11 +59,14 @@ class TestSettings:
|
|||||||
'tor_bridges_use_obfs4': False,
|
'tor_bridges_use_obfs4': False,
|
||||||
'tor_bridges_use_meek_lite_azure': False,
|
'tor_bridges_use_meek_lite_azure': False,
|
||||||
'tor_bridges_use_custom_bridges': '',
|
'tor_bridges_use_custom_bridges': '',
|
||||||
|
'use_legacy_v2_onions': False,
|
||||||
'save_private_key': False,
|
'save_private_key': False,
|
||||||
'private_key': '',
|
'private_key': '',
|
||||||
'slug': '',
|
'slug': '',
|
||||||
'hidservauth_string': '',
|
'hidservauth_string': '',
|
||||||
'downloads_dir': os.path.expanduser('~/OnionShare')
|
'downloads_dir': os.path.expanduser('~/OnionShare'),
|
||||||
|
'receive_allow_receiver_shutdown': True,
|
||||||
|
'public_mode': False
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_fill_in_defaults(self, settings_obj):
|
def test_fill_in_defaults(self, settings_obj):
|
||||||
@ -118,7 +120,6 @@ class TestSettings:
|
|||||||
assert settings_obj.get('auth_type') == 'no_auth'
|
assert settings_obj.get('auth_type') == 'no_auth'
|
||||||
assert settings_obj.get('auth_password') == ''
|
assert settings_obj.get('auth_password') == ''
|
||||||
assert settings_obj.get('close_after_first_download') is True
|
assert settings_obj.get('close_after_first_download') is True
|
||||||
assert settings_obj.get('systray_notifications') is True
|
|
||||||
assert settings_obj.get('use_stealth') is False
|
assert settings_obj.get('use_stealth') is False
|
||||||
assert settings_obj.get('use_autoupdate') is True
|
assert settings_obj.get('use_autoupdate') is True
|
||||||
assert settings_obj.get('autoupdate_timestamp') is None
|
assert settings_obj.get('autoupdate_timestamp') is None
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
OnionShare | https://onionshare.org/
|
OnionShare | https://onionshare.org/
|
||||||
|
|
||||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -26,14 +26,179 @@ import re
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from onionshare.common import Common
|
from onionshare.common import Common
|
||||||
|
from onionshare.web import Web
|
||||||
|
from onionshare.settings import Settings
|
||||||
|
|
||||||
DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
|
DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
|
||||||
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
|
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
|
||||||
|
|
||||||
|
|
||||||
|
def web_obj(common_obj, receive_mode, num_files=0):
|
||||||
|
""" Creates a Web object, in either share mode or receive mode, ready for testing """
|
||||||
|
common_obj.load_settings()
|
||||||
|
|
||||||
|
web = Web(common_obj, False, receive_mode)
|
||||||
|
web.generate_slug()
|
||||||
|
web.stay_open = True
|
||||||
|
web.running = True
|
||||||
|
|
||||||
|
web.app.testing = True
|
||||||
|
|
||||||
|
# Share mode
|
||||||
|
if not receive_mode:
|
||||||
|
# Add files
|
||||||
|
files = []
|
||||||
|
for i in range(num_files):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
||||||
|
tmp_file.write(b'*' * 1024)
|
||||||
|
files.append(tmp_file.name)
|
||||||
|
web.set_file_info(files)
|
||||||
|
# Receive mode
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return web
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeb:
|
||||||
|
def test_share_mode(self, common_obj):
|
||||||
|
web = web_obj(common_obj, False, 3)
|
||||||
|
assert web.receive_mode is False
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Load 404 pages
|
||||||
|
res = c.get('/')
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
res = c.get('/invalidslug'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Load download page
|
||||||
|
res = c.get('/{}'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
# Download
|
||||||
|
res = c.get('/{}/download'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert res.mimetype == 'application/zip'
|
||||||
|
|
||||||
|
def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024):
|
||||||
|
web = web_obj(common_obj, False, 3)
|
||||||
|
web.stay_open = False
|
||||||
|
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Download the first time
|
||||||
|
res = c.get('/{}/download'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert res.mimetype == 'application/zip'
|
||||||
|
|
||||||
|
assert web.running == False
|
||||||
|
|
||||||
|
def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024):
|
||||||
|
web = web_obj(common_obj, False, 3)
|
||||||
|
web.stay_open = True
|
||||||
|
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Download the first time
|
||||||
|
res = c.get('/{}/download'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert res.mimetype == 'application/zip'
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
def test_receive_mode(self, common_obj):
|
||||||
|
web = web_obj(common_obj, True)
|
||||||
|
assert web.receive_mode is True
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Load 404 pages
|
||||||
|
res = c.get('/')
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
res = c.get('/invalidslug'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Load upload page
|
||||||
|
res = c.get('/{}'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
def test_receive_mode_allow_receiver_shutdown_on(self, common_obj):
|
||||||
|
web = web_obj(common_obj, True)
|
||||||
|
|
||||||
|
common_obj.settings.set('receive_allow_receiver_shutdown', True)
|
||||||
|
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Load close page
|
||||||
|
res = c.post('/{}/close'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
# Should return ok, and server should stop
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert web.running == False
|
||||||
|
|
||||||
|
def test_receive_mode_allow_receiver_shutdown_off(self, common_obj):
|
||||||
|
web = web_obj(common_obj, True)
|
||||||
|
|
||||||
|
common_obj.settings.set('receive_allow_receiver_shutdown', False)
|
||||||
|
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Load close page
|
||||||
|
res = c.post('/{}/close'.format(web.slug))
|
||||||
|
res.get_data()
|
||||||
|
# Should redirect to index, and server should still be running
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert web.running == True
|
||||||
|
|
||||||
|
def test_public_mode_on(self, common_obj):
|
||||||
|
web = web_obj(common_obj, True)
|
||||||
|
common_obj.settings.set('public_mode', True)
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# Upload page should be accessible from /
|
||||||
|
res = c.get('/')
|
||||||
|
data1 = res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
# /[slug] should be a 404
|
||||||
|
res = c.get('/{}'.format(web.slug))
|
||||||
|
data2 = res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
def test_public_mode_off(self, common_obj):
|
||||||
|
web = web_obj(common_obj, True)
|
||||||
|
common_obj.settings.set('public_mode', False)
|
||||||
|
|
||||||
|
with web.app.test_client() as c:
|
||||||
|
# / should be a 404
|
||||||
|
res = c.get('/')
|
||||||
|
data1 = res.get_data()
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Upload page should be accessible from /[slug]
|
||||||
|
res = c.get('/{}'.format(web.slug))
|
||||||
|
data2 = res.get_data()
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
class TestZipWriterDefault:
|
class TestZipWriterDefault:
|
||||||
@pytest.mark.parametrize('test_input', (
|
@pytest.mark.parametrize('test_input', (
|
||||||
'onionshare_{}.zip'.format(''.join(
|
'onionshare_{}.zip'.format(''.join(
|
||||||
|