diff --git a/.travis.yml b/.travis.yml index 6d324010..9010e77a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,12 @@ python: - "nightly" # command to install dependencies install: - - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls + - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8 +before_script: + # stop the build if there are Python syntax errors or undefined names + - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ after_success: diff --git a/install/onionshare.desktop b/install/onionshare.desktop index 256a7b50..fbac3660 100644 --- a/install/onionshare.desktop +++ b/install/onionshare.desktop @@ -1,9 +1,11 @@ [Desktop Entry] Name=OnionShare Comment=Share a file securely and anonymously over Tor +Comment[da]=Del en fil sikkert og anonymt over Tor Exec=/usr/bin/onionshare-gui Terminal=false Type=Application Icon=/usr/share/pixmaps/onionshare80.xpm Categories=Network; Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting; +Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting; diff --git a/install/onionshare.nsi b/install/onionshare.nsi index 0a9e6cf2..5b53b048 100644 --- a/install/onionshare.nsi +++ b/install/onionshare.nsi @@ -201,6 +201,10 @@ Section "install" File "${BINPATH}\share\html\index.html" SetOutPath "$INSTDIR\share\images" + File "${BINPATH}\share\images\download_completed.png" + File "${BINPATH}\share\images\download_completed_none.png" + File "${BINPATH}\share\images\download_in_progress.png" + File "${BINPATH}\share\images\download_in_progress_none.png" File "${BINPATH}\share\images\favicon.ico" File "${BINPATH}\share\images\file_delete.png" File "${BINPATH}\share\images\info.png" @@ -383,6 +387,10 @@ FunctionEnd Delete "$INSTDIR\share\html\404.html" Delete "$INSTDIR\share\html\denied.html" Delete "$INSTDIR\share\html\index.html" + Delete "$INSTDIR\share\images\download_completed.png" + Delete "$INSTDIR\share\images\download_completed_none.png" + Delete "$INSTDIR\share\images\download_in_progress.png" + Delete "$INSTDIR\share\images\download_in_progress_none.png" Delete "$INSTDIR\share\images\favicon.ico" Delete "$INSTDIR\share\images\file_delete.png" Delete "$INSTDIR\share\images\info.png" diff --git a/install/scripts/onionshare-nautilus.py b/install/scripts/onionshare-nautilus.py index 6674dd18..ed50fb23 100644 --- a/install/scripts/onionshare-nautilus.py +++ b/install/scripts/onionshare-nautilus.py @@ -64,7 +64,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider): """ def url2path(self,url): - file_uri = url.get_activation_uri() + file_uri = url.get_activation_uri() arg_uri = file_uri[7:] path = urllib.url2pathname(arg_uri) return path diff --git a/onionshare/common.py b/onionshare/common.py index 25b901ee..79d62ca9 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -23,6 +23,7 @@ import inspect import os import platform import random +import re import socket import sys import tempfile @@ -56,8 +57,10 @@ def get_platform(): """ Returns the platform OnionShare is running on. """ - return platform.system() - + plat = platform.system() + if re.match('^.*BSD$', plat): + plat = 'BSD' + return plat def get_resource_path(filename): """ @@ -73,7 +76,7 @@ def get_resource_path(filename): # While running tests during stdeb bdist_deb, look 3 directories up for the share folder prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share') - elif p == 'Linux': + elif p == 'BSD' or p == 'Linux': # Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode prefix = os.path.join(sys.prefix, 'share/onionshare') @@ -107,7 +110,7 @@ def get_tor_paths(): tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip') tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6') obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy') - elif p == 'OpenBSD' or p == 'FreeBSD': + elif p == 'BSD': tor_path = '/usr/local/bin/tor' tor_geo_ip_file_path = '/usr/local/share/tor/geoip' tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6' diff --git a/onionshare/onion.py b/onionshare/onion.py index 668de051..41e44781 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -131,7 +131,7 @@ class Onion(object): self.stealth = False self.service_id = None - self.system = platform.system() + self.system = common.get_platform() # Is bundled tor supported? if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False): @@ -183,7 +183,7 @@ class Onion(object): raise OSError(strings._('no_available_port')) self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc') else: - # Linux and Mac can use unix sockets + # Linux, Mac and BSD can use unix sockets with open(common.get_resource_path('torrc_template')) as f: torrc_template = f.read() self.tor_control_port = None @@ -318,7 +318,7 @@ class Onion(object): # guessing the socket file name next if not found_tor: try: - if self.system == 'Linux': + if self.system == 'Linux' or self.system == 'BSD': socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) elif self.system == 'Darwin': socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) diff --git a/onionshare/web.py b/onionshare/web.py index 0fedb29b..d16ca251 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -315,12 +315,14 @@ def download(slug_candidate): percent = (1.0 * downloaded_bytes / zip_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not gui_mode or common.get_platform() == 'Linux': + plat = common.get_platform() + if not gui_mode or plat == 'Linux' or plat == 'BSD': sys.stdout.write( "\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent)) sys.stdout.flush() add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes}) + done = False except: # looks like the download was canceled done = True diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 14c76617..24e627bb 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -35,8 +35,8 @@ class Application(QtWidgets.QApplication): and the quick keyboard shortcut. """ def __init__(self): - system = platform.system() - if system == 'Linux': + system = common.get_platform() + if system == 'Linux' or system == 'BSD': self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) QtWidgets.QApplication.__init__(self, sys.argv) self.installEventFilter(self) diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py index 6a9d2e21..f08073db 100644 --- a/onionshare_gui/file_selection.py +++ b/onionshare_gui/file_selection.py @@ -268,10 +268,6 @@ class FileSelection(QtWidgets.QVBoxLayout): super(FileSelection, self).__init__() self.server_on = False - # Info label - self.info_label = QtWidgets.QLabel() - self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') - # File list self.file_list = FileList() self.file_list.currentItemChanged.connect(self.update) @@ -289,7 +285,6 @@ class FileSelection(QtWidgets.QVBoxLayout): button_layout.addWidget(self.delete_button) # Add the widgets - self.addWidget(self.info_label) self.addWidget(self.file_list) self.addLayout(button_layout) @@ -299,23 +294,6 @@ class FileSelection(QtWidgets.QVBoxLayout): """ Update the GUI elements based on the current state. """ - # Update the info label - file_count = self.file_list.count() - if file_count == 0: - self.info_label.hide() - else: - total_size_bytes = 0 - for index in range(self.file_list.count()): - item = self.file_list.item(index) - total_size_bytes += item.size_bytes - total_size_readable = 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)) - self.info_label.show() - # All buttons should be hidden if the server is on if self.server_on: self.add_button.hide() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index af90ad58..50f920eb 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -109,6 +109,30 @@ class OnionShareGui(QtWidgets.QMainWindow): self.vbar = self.downloads_container.verticalScrollBar() self.downloads_container.hide() # downloads start out hidden 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_in_progress_download_count = QtWidgets.QLabel() + self.info_in_progress_download_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') + self.info_in_progress_download_count.hide() + + self.info_completed_downloads_count = QtWidgets.QLabel() + self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') + self.info_completed_downloads_count.hide() + + self.info_layout.addWidget(self.info_label) + self.info_layout.addStretch() + self.info_layout.addWidget(self.info_in_progress_download_count) + self.info_layout.addWidget(self.info_completed_downloads_count) + + 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() @@ -169,6 +193,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # 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() @@ -200,10 +225,26 @@ class OnionShareGui(QtWidgets.QMainWindow): def update_primary_action(self): # Show or hide primary action layout - if self.file_selection.file_list.count() > 0: + file_count = self.file_selection.file_list.count() + if file_count > 0: self.primary_action.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 = 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)) + self.info_widget.show() + else: self.primary_action.hide() + self.info_widget.hide() # Resize window self.adjustSize() @@ -324,6 +365,9 @@ class OnionShareGui(QtWidgets.QMainWindow): # Hide and reset the downloads if we have previously shared self.downloads_container.hide() self.downloads.reset_downloads() + self.reset_info_counters() + self.info_in_progress_download_count.show() + self.info_completed_downloads_count.show() self.status_bar.clearMessage() self.server_share_status_label.setText('') @@ -459,6 +503,9 @@ class OnionShareGui(QtWidgets.QMainWindow): # 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) @@ -528,6 +575,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.downloads_container.show() # show the downloads layout self.downloads.add_download(event["data"]["id"], 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.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) @@ -542,6 +591,13 @@ class OnionShareGui(QtWidgets.QMainWindow): if event["data"]["bytes"] == web.zip_filesize: if self.systemTray.supportsMessages() and self.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? if not web.get_stay_open(): self.server_status.stop_server() @@ -550,9 +606,15 @@ class OnionShareGui(QtWidgets.QMainWindow): 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_CANCELED: self.downloads.cancel_download(event["data"]["id"]) + # 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.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) @@ -608,6 +670,37 @@ class OnionShareGui(QtWidgets.QMainWindow): # Disable settings menu action when server is active self.settingsAction.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_in_progress_download_count.show() + self.info_completed_downloads_count.show() + + def update_downloads_completed(self, count): + """ + Update the 'Downloads completed' info widget. + """ + if count == 0: + self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png') + else: + self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png') + self.info_completed_downloads_count.setText(' {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_download_image = common.get_resource_path('images/download_in_progress_none.png') + else: + self.info_in_progress_download_image = common.get_resource_path('images/download_in_progress.png') + self.info_in_progress_download_count.setText(' {1:d}'.format(self.info_in_progress_download_image, count)) + self.info_in_progress_download_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count)) + def closeEvent(self, e): common.log('OnionShareGui', 'closeEvent') try: diff --git a/setup.py b/setup.py index dcbe42fb..23e1ea17 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,17 @@ author_email = 'micah@micahflee.com' url = 'https://github.com/micahflee/onionshare' license = 'GPL v3' keywords = 'onion, share, onionshare, tor, anonymous, web server' +data_files=[ + (os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']), + (os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']), + (os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']), + (os.path.join(sys.prefix, 'share/onionshare'), file_list('share')), + (os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')), + (os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')), + (os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')), + ] +if platform.system() != 'OpenBSD': + data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py'])) setup( name='onionshare', version=version, @@ -54,14 +65,5 @@ setup( packages=['onionshare', 'onionshare_gui'], include_package_data=True, scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], - data_files=[ - (os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']), - (os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']), - (os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']), - (os.path.join(sys.prefix, 'share/onionshare'), file_list('share')), - (os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')), - (os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')), - (os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')), - ('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']) - ] + data_files=data_files ) diff --git a/share/images/download_completed.png b/share/images/download_completed.png new file mode 100644 index 00000000..e68fe5a2 Binary files /dev/null and b/share/images/download_completed.png differ diff --git a/share/images/download_completed_none.png b/share/images/download_completed_none.png new file mode 100644 index 00000000..8dbd6939 Binary files /dev/null and b/share/images/download_completed_none.png differ diff --git a/share/images/download_in_progress.png b/share/images/download_in_progress.png new file mode 100644 index 00000000..19694659 Binary files /dev/null and b/share/images/download_in_progress.png differ diff --git a/share/images/download_in_progress_none.png b/share/images/download_in_progress_none.png new file mode 100644 index 00000000..2d61dba4 Binary files /dev/null and b/share/images/download_in_progress_none.png differ diff --git a/share/locale/en.json b/share/locale/en.json index d703fd0c..6a4b7430 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -145,5 +145,7 @@ "gui_status_indicator_working": "Starting...", "gui_status_indicator_started": "Sharing", "gui_file_info": "{} Files, {}", - "gui_file_info_single": "{} File, {}" + "gui_file_info_single": "{} File, {}", + "info_in_progress_downloads_tooltip": "{} download(s) in progress", + "info_completed_downloads_tooltip": "{} download(s) completed" }