Merge pull request #1020 from micahflee/991_sharing_code
[WIP] Share code between share mode and website mode
@ -109,6 +109,8 @@ def main(cwd=None):
|
||||
# Re-load settings, if a custom config was passed in
|
||||
if config:
|
||||
common.load_settings(config)
|
||||
else:
|
||||
common.load_settings()
|
||||
|
||||
# Verbose mode?
|
||||
common.verbose = verbose
|
||||
@ -260,12 +262,12 @@ def main(cwd=None):
|
||||
if not app.autostop_timer_thread.is_alive():
|
||||
if mode == 'share' or (mode == 'website'):
|
||||
# If there were no attempts to download the share, or all downloads are done, we can stop
|
||||
if web.share_mode.download_count == 0 or web.done:
|
||||
if web.share_mode.cur_history_id == 0 or web.done:
|
||||
print("Stopped because auto-stop timer ran out")
|
||||
web.stop(app.port)
|
||||
break
|
||||
if mode == 'receive':
|
||||
if web.receive_mode.upload_count == 0 or not web.receive_mode.uploads_in_progress:
|
||||
if web.receive_mode.cur_history_id == 0 or not web.receive_mode.uploads_in_progress:
|
||||
print("Stopped because auto-stop timer ran out")
|
||||
web.stop(app.port)
|
||||
break
|
||||
|
@ -203,7 +203,7 @@ class Common(object):
|
||||
border: 0px;
|
||||
}""",
|
||||
|
||||
# Common styles between ShareMode and ReceiveMode and their child widgets
|
||||
# Common styles between modes and their child widgets
|
||||
'mode_info_label': """
|
||||
QLabel {
|
||||
font-size: 12px;
|
||||
@ -310,6 +310,21 @@ class Common(object):
|
||||
width: 10px;
|
||||
}""",
|
||||
|
||||
'history_individual_file_timestamp_label': """
|
||||
QLabel {
|
||||
color: #666666;
|
||||
}""",
|
||||
|
||||
'history_individual_file_status_code_label_2xx': """
|
||||
QLabel {
|
||||
color: #008800;
|
||||
}""",
|
||||
|
||||
'history_individual_file_status_code_label_4xx': """
|
||||
QLabel {
|
||||
color: #cc0000;
|
||||
}""",
|
||||
|
||||
# Share mode and child widget styles
|
||||
'share_zip_progess_bar': """
|
||||
QProgressBar {
|
||||
|
@ -8,7 +8,7 @@ from werkzeug.utils import secure_filename
|
||||
from .. import strings
|
||||
|
||||
|
||||
class ReceiveModeWeb(object):
|
||||
class ReceiveModeWeb:
|
||||
"""
|
||||
All of the web logic for receive mode
|
||||
"""
|
||||
@ -18,13 +18,12 @@ class ReceiveModeWeb(object):
|
||||
|
||||
self.web = web
|
||||
|
||||
# Reset assets path
|
||||
self.web.app.static_folder=self.common.get_resource_path('static')
|
||||
|
||||
self.can_upload = True
|
||||
self.upload_count = 0
|
||||
self.uploads_in_progress = []
|
||||
|
||||
# This tracks the history id
|
||||
self.cur_history_id = 0
|
||||
|
||||
self.define_routes()
|
||||
|
||||
def define_routes(self):
|
||||
@ -33,8 +32,15 @@ class ReceiveModeWeb(object):
|
||||
"""
|
||||
@self.web.app.route("/")
|
||||
def index():
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
|
||||
'id': history_id,
|
||||
'status_code': 200
|
||||
})
|
||||
|
||||
self.web.add_request(self.web.REQUEST_LOAD, request.path)
|
||||
r = make_response(render_template('receive.html',
|
||||
r = make_response(render_template('receive.html',
|
||||
static_url_path=self.web.static_url_path))
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
@ -55,7 +61,7 @@ class ReceiveModeWeb(object):
|
||||
|
||||
# Tell the GUI the receive mode directory for this file
|
||||
self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, {
|
||||
'id': request.upload_id,
|
||||
'id': request.history_id,
|
||||
'filename': basename,
|
||||
'dir': request.receive_mode_dir
|
||||
})
|
||||
@ -275,10 +281,9 @@ class ReceiveModeRequest(Request):
|
||||
# Prevent new uploads if we've said so (timer expired)
|
||||
if self.web.receive_mode.can_upload:
|
||||
|
||||
# Create an upload_id, attach it to the request
|
||||
self.upload_id = self.web.receive_mode.upload_count
|
||||
|
||||
self.web.receive_mode.upload_count += 1
|
||||
# Create an history_id, attach it to the request
|
||||
self.history_id = self.web.receive_mode.cur_history_id
|
||||
self.web.receive_mode.cur_history_id += 1
|
||||
|
||||
# Figure out the content length
|
||||
try:
|
||||
@ -305,10 +310,10 @@ class ReceiveModeRequest(Request):
|
||||
if not self.told_gui_about_request:
|
||||
# Tell the GUI about the request
|
||||
self.web.add_request(self.web.REQUEST_STARTED, self.path, {
|
||||
'id': self.upload_id,
|
||||
'id': self.history_id,
|
||||
'content_length': self.content_length
|
||||
})
|
||||
self.web.receive_mode.uploads_in_progress.append(self.upload_id)
|
||||
self.web.receive_mode.uploads_in_progress.append(self.history_id)
|
||||
|
||||
self.told_gui_about_request = True
|
||||
|
||||
@ -340,19 +345,19 @@ class ReceiveModeRequest(Request):
|
||||
|
||||
try:
|
||||
if self.told_gui_about_request:
|
||||
upload_id = self.upload_id
|
||||
history_id = self.history_id
|
||||
|
||||
if not self.web.stop_q.empty() or not self.progress[self.filename]['complete']:
|
||||
# Inform the GUI that the upload has canceled
|
||||
self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, {
|
||||
'id': upload_id
|
||||
'id': history_id
|
||||
})
|
||||
else:
|
||||
# Inform the GUI that the upload has finished
|
||||
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
|
||||
'id': upload_id
|
||||
'id': history_id
|
||||
})
|
||||
self.web.receive_mode.uploads_in_progress.remove(upload_id)
|
||||
self.web.receive_mode.uploads_in_progress.remove(history_id)
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -378,7 +383,7 @@ class ReceiveModeRequest(Request):
|
||||
# Update the GUI on the upload progress
|
||||
if self.told_gui_about_request:
|
||||
self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
|
||||
'id': self.upload_id,
|
||||
'id': self.history_id,
|
||||
'progress': self.progress
|
||||
})
|
||||
|
||||
|
270
onionshare/web/send_base_mode.py
Normal file
@ -0,0 +1,270 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import mimetypes
|
||||
import gzip
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
from .. import strings
|
||||
|
||||
|
||||
class SendBaseModeWeb:
|
||||
"""
|
||||
All of the web logic shared between share and website mode (modes where the user sends files)
|
||||
"""
|
||||
def __init__(self, common, web):
|
||||
super(SendBaseModeWeb, self).__init__()
|
||||
self.common = common
|
||||
self.web = web
|
||||
|
||||
# Information about the file to be shared
|
||||
self.is_zipped = False
|
||||
self.download_filename = None
|
||||
self.download_filesize = None
|
||||
self.gzip_filename = None
|
||||
self.gzip_filesize = None
|
||||
self.zip_writer = None
|
||||
|
||||
# If "Stop After First Download" is checked (stay_open == False), only allow
|
||||
# one download at a time.
|
||||
self.download_in_progress = False
|
||||
|
||||
# This tracks the history id
|
||||
self.cur_history_id = 0
|
||||
|
||||
self.define_routes()
|
||||
self.init()
|
||||
|
||||
def set_file_info(self, filenames, processed_size_callback=None):
|
||||
"""
|
||||
Build a data structure that describes the list of files
|
||||
"""
|
||||
# If there's just one folder, replace filenames with a list of files inside that folder
|
||||
if len(filenames) == 1 and os.path.isdir(filenames[0]):
|
||||
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
|
||||
|
||||
# Re-initialize
|
||||
self.files = {} # Dictionary mapping file paths to filenames on disk
|
||||
self.root_files = {} # This is only the root files and dirs, as opposed to all of them
|
||||
self.cleanup_filenames = []
|
||||
self.cur_history_id = 0
|
||||
self.file_info = {'files': [], 'dirs': []}
|
||||
self.gzip_individual_files = {}
|
||||
self.init()
|
||||
|
||||
# Build the file list
|
||||
for filename in filenames:
|
||||
basename = os.path.basename(filename.rstrip('/'))
|
||||
|
||||
# If it's a filename, add it
|
||||
if os.path.isfile(filename):
|
||||
self.files[basename] = filename
|
||||
self.root_files[basename] = filename
|
||||
|
||||
# If it's a directory, add it recursively
|
||||
elif os.path.isdir(filename):
|
||||
self.root_files[basename + '/'] = filename
|
||||
|
||||
for root, _, nested_filenames in os.walk(filename):
|
||||
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
|
||||
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
|
||||
# The normalized_root should be "some_folder/foobar"
|
||||
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
|
||||
|
||||
# Add the dir itself
|
||||
self.files[normalized_root + '/'] = root
|
||||
|
||||
# Add the files in this dir
|
||||
for nested_filename in nested_filenames:
|
||||
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
|
||||
|
||||
self.set_file_info_custom(filenames, processed_size_callback)
|
||||
|
||||
def directory_listing(self, filenames, path='', filesystem_path=None):
|
||||
# Tell the GUI about the directory listing
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '/{}'.format(path), {
|
||||
'id': history_id,
|
||||
'method': request.method,
|
||||
'status_code': 200
|
||||
})
|
||||
|
||||
# If filesystem_path is None, this is the root directory listing
|
||||
files, dirs = self.build_directory_listing(filenames, filesystem_path)
|
||||
r = self.directory_listing_template(path, files, dirs)
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
def build_directory_listing(self, filenames, filesystem_path):
|
||||
files = []
|
||||
dirs = []
|
||||
|
||||
for filename in filenames:
|
||||
if filesystem_path:
|
||||
this_filesystem_path = os.path.join(filesystem_path, filename)
|
||||
else:
|
||||
this_filesystem_path = self.files[filename]
|
||||
|
||||
is_dir = os.path.isdir(this_filesystem_path)
|
||||
|
||||
if is_dir:
|
||||
dirs.append({
|
||||
'basename': filename
|
||||
})
|
||||
else:
|
||||
size = os.path.getsize(this_filesystem_path)
|
||||
size_human = self.common.human_readable_filesize(size)
|
||||
files.append({
|
||||
'basename': filename,
|
||||
'size_human': size_human
|
||||
})
|
||||
return files, dirs
|
||||
|
||||
def stream_individual_file(self, filesystem_path):
|
||||
"""
|
||||
Return a flask response that's streaming the download of an individual file, and gzip
|
||||
compressing it if the browser supports it.
|
||||
"""
|
||||
use_gzip = self.should_use_gzip()
|
||||
|
||||
# gzip compress the individual file, if it hasn't already been compressed
|
||||
if use_gzip:
|
||||
if filesystem_path not in self.gzip_individual_files:
|
||||
gzip_filename = tempfile.mkstemp('wb+')[1]
|
||||
self._gzip_compress(filesystem_path, gzip_filename, 6, None)
|
||||
self.gzip_individual_files[filesystem_path] = gzip_filename
|
||||
|
||||
# Make sure the gzip file gets cleaned up when onionshare stops
|
||||
self.cleanup_filenames.append(gzip_filename)
|
||||
|
||||
file_to_download = self.gzip_individual_files[filesystem_path]
|
||||
filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
|
||||
else:
|
||||
file_to_download = filesystem_path
|
||||
filesize = os.path.getsize(filesystem_path)
|
||||
|
||||
path = request.path
|
||||
|
||||
# Tell GUI the individual file started
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, path, {
|
||||
'id': history_id,
|
||||
'filesize': filesize
|
||||
})
|
||||
|
||||
# Only GET requests are allowed, any other method should fail
|
||||
if request.method != "GET":
|
||||
return self.web.error405()
|
||||
|
||||
def generate():
|
||||
chunk_size = 102400 # 100kb
|
||||
|
||||
fp = open(file_to_download, 'rb')
|
||||
done = False
|
||||
while not done:
|
||||
chunk = fp.read(chunk_size)
|
||||
if chunk == b'':
|
||||
done = True
|
||||
else:
|
||||
try:
|
||||
yield chunk
|
||||
|
||||
# Tell GUI the progress
|
||||
downloaded_bytes = fp.tell()
|
||||
percent = (1.0 * downloaded_bytes / filesize) * 100
|
||||
if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
|
||||
sys.stdout.write(
|
||||
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
|
||||
sys.stdout.flush()
|
||||
|
||||
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS, path, {
|
||||
'id': history_id,
|
||||
'bytes': downloaded_bytes,
|
||||
'filesize': filesize
|
||||
})
|
||||
done = False
|
||||
except:
|
||||
# Looks like the download was canceled
|
||||
done = True
|
||||
|
||||
# Tell the GUI the individual file was canceled
|
||||
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_CANCELED, path, {
|
||||
'id': history_id
|
||||
})
|
||||
|
||||
fp.close()
|
||||
|
||||
if self.common.platform != 'Darwin':
|
||||
sys.stdout.write("\n")
|
||||
|
||||
basename = os.path.basename(filesystem_path)
|
||||
|
||||
r = Response(generate())
|
||||
if use_gzip:
|
||||
r.headers.set('Content-Encoding', 'gzip')
|
||||
r.headers.set('Content-Length', filesize)
|
||||
r.headers.set('Content-Disposition', 'inline', filename=basename)
|
||||
r = self.web.add_security_headers(r)
|
||||
(content_type, _) = mimetypes.guess_type(basename, strict=False)
|
||||
if content_type is not None:
|
||||
r.headers.set('Content-Type', content_type)
|
||||
return r
|
||||
|
||||
def should_use_gzip(self):
|
||||
"""
|
||||
Should we use gzip for this browser?
|
||||
"""
|
||||
return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
|
||||
|
||||
def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
|
||||
"""
|
||||
Compress a file with gzip, without loading the whole thing into memory
|
||||
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
|
||||
"""
|
||||
bytes_processed = 0
|
||||
blocksize = 1 << 16 # 64kB
|
||||
with open(input_filename, 'rb') as input_file:
|
||||
output_file = gzip.open(output_filename, 'wb', level)
|
||||
while True:
|
||||
if processed_size_callback is not None:
|
||||
processed_size_callback(bytes_processed)
|
||||
|
||||
block = input_file.read(blocksize)
|
||||
if len(block) == 0:
|
||||
break
|
||||
output_file.write(block)
|
||||
bytes_processed += blocksize
|
||||
|
||||
output_file.close()
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
Inherited class will implement this
|
||||
"""
|
||||
pass
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
Inherited class will implement this
|
||||
"""
|
||||
pass
|
||||
|
||||
def directory_listing_template(self):
|
||||
"""
|
||||
Inherited class will implement this. It should call render_template and return
|
||||
the response.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
"""
|
||||
Inherited class will implement this.
|
||||
"""
|
||||
pass
|
||||
|
||||
def render_logic(self, path=''):
|
||||
"""
|
||||
Inherited class will implement this.
|
||||
"""
|
||||
pass
|
@ -3,55 +3,35 @@ import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import mimetypes
|
||||
import gzip
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
from .send_base_mode import SendBaseModeWeb
|
||||
from .. import strings
|
||||
|
||||
|
||||
class ShareModeWeb(object):
|
||||
class ShareModeWeb(SendBaseModeWeb):
|
||||
"""
|
||||
All of the web logic for share mode
|
||||
"""
|
||||
def __init__(self, common, web):
|
||||
self.common = common
|
||||
self.common.log('ShareModeWeb', '__init__')
|
||||
def init(self):
|
||||
self.common.log('ShareModeWeb', 'init')
|
||||
|
||||
self.web = web
|
||||
|
||||
# Information about the file to be shared
|
||||
self.file_info = []
|
||||
self.is_zipped = False
|
||||
self.download_filename = None
|
||||
self.download_filesize = None
|
||||
self.gzip_filename = None
|
||||
self.gzip_filesize = None
|
||||
self.zip_writer = None
|
||||
|
||||
self.download_count = 0
|
||||
|
||||
# If "Stop After First Download" is checked (stay_open == False), only allow
|
||||
# one download at a time.
|
||||
self.download_in_progress = False
|
||||
|
||||
# Reset assets path
|
||||
self.web.app.static_folder=self.common.get_resource_path('static')
|
||||
|
||||
|
||||
self.define_routes()
|
||||
# Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
|
||||
self.download_individual_files = not self.common.settings.get('close_after_first_download')
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
The web app routes for sharing files
|
||||
"""
|
||||
@self.web.app.route("/")
|
||||
def index():
|
||||
@self.web.app.route('/', defaults={'path': ''})
|
||||
@self.web.app.route('/<path:path>')
|
||||
def index(path):
|
||||
"""
|
||||
Render the template for the onionshare landing page.
|
||||
"""
|
||||
self.web.add_request(self.web.REQUEST_LOAD, request.path)
|
||||
|
||||
# Deny new downloads if "Stop After First Download" is checked and there is
|
||||
# Deny new downloads if "Stop sharing after files have been sent" is checked and there is
|
||||
# currently a download
|
||||
deny_download = not self.web.stay_open and self.download_in_progress
|
||||
if deny_download:
|
||||
@ -65,15 +45,7 @@ class ShareModeWeb(object):
|
||||
else:
|
||||
self.filesize = self.download_filesize
|
||||
|
||||
r = make_response(render_template(
|
||||
'send.html',
|
||||
file_info=self.file_info,
|
||||
filename=os.path.basename(self.download_filename),
|
||||
filesize=self.filesize,
|
||||
filesize_human=self.common.human_readable_filesize(self.download_filesize),
|
||||
is_zipped=self.is_zipped,
|
||||
static_url_path=self.web.static_url_path))
|
||||
return self.web.add_security_headers(r)
|
||||
return self.render_logic(path)
|
||||
|
||||
@self.web.app.route("/download")
|
||||
def download():
|
||||
@ -88,10 +60,6 @@ class ShareModeWeb(object):
|
||||
static_url_path=self.web.static_url_path))
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
# Each download has a unique id
|
||||
download_id = self.download_count
|
||||
self.download_count += 1
|
||||
|
||||
# Prepare some variables to use inside generate() function below
|
||||
# which is outside of the request context
|
||||
shutdown_func = request.environ.get('werkzeug.server.shutdown')
|
||||
@ -109,8 +77,10 @@ class ShareModeWeb(object):
|
||||
self.filesize = self.download_filesize
|
||||
|
||||
# Tell GUI the download started
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(self.web.REQUEST_STARTED, path, {
|
||||
'id': download_id,
|
||||
'id': history_id,
|
||||
'use_gzip': use_gzip
|
||||
})
|
||||
|
||||
@ -130,7 +100,7 @@ class ShareModeWeb(object):
|
||||
# The user has canceled the download, so stop serving the file
|
||||
if not self.web.stop_q.empty():
|
||||
self.web.add_request(self.web.REQUEST_CANCELED, path, {
|
||||
'id': download_id
|
||||
'id': history_id
|
||||
})
|
||||
break
|
||||
|
||||
@ -152,7 +122,7 @@ class ShareModeWeb(object):
|
||||
sys.stdout.flush()
|
||||
|
||||
self.web.add_request(self.web.REQUEST_PROGRESS, path, {
|
||||
'id': download_id,
|
||||
'id': history_id,
|
||||
'bytes': downloaded_bytes
|
||||
})
|
||||
self.web.done = False
|
||||
@ -163,7 +133,7 @@ class ShareModeWeb(object):
|
||||
|
||||
# tell the GUI the download has canceled
|
||||
self.web.add_request(self.web.REQUEST_CANCELED, path, {
|
||||
'id': download_id
|
||||
'id': history_id
|
||||
})
|
||||
|
||||
fp.close()
|
||||
@ -198,19 +168,71 @@ class ShareModeWeb(object):
|
||||
r.headers.set('Content-Type', content_type)
|
||||
return r
|
||||
|
||||
def set_file_info(self, filenames, processed_size_callback=None):
|
||||
"""
|
||||
Using the list of filenames being shared, fill in details that the web
|
||||
page will need to display. This includes zipping up the file in order to
|
||||
get the zip file's name and size.
|
||||
"""
|
||||
self.common.log("ShareModeWeb", "set_file_info")
|
||||
def directory_listing_template(self, path, files, dirs):
|
||||
return make_response(render_template(
|
||||
'send.html',
|
||||
file_info=self.file_info,
|
||||
files=files,
|
||||
dirs=dirs,
|
||||
filename=os.path.basename(self.download_filename),
|
||||
filesize=self.filesize,
|
||||
filesize_human=self.common.human_readable_filesize(self.download_filesize),
|
||||
is_zipped=self.is_zipped,
|
||||
static_url_path=self.web.static_url_path,
|
||||
download_individual_files=self.download_individual_files))
|
||||
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
self.common.log("ShareModeWeb", "set_file_info_custom")
|
||||
self.web.cancel_compression = False
|
||||
self.build_zipfile_list(filenames, processed_size_callback)
|
||||
|
||||
self.cleanup_filenames = []
|
||||
def render_logic(self, path=''):
|
||||
if path in self.files:
|
||||
filesystem_path = self.files[path]
|
||||
|
||||
# build file info list
|
||||
self.file_info = {'files': [], 'dirs': []}
|
||||
# If it's a directory
|
||||
if os.path.isdir(filesystem_path):
|
||||
# Render directory listing
|
||||
filenames = []
|
||||
for filename in os.listdir(filesystem_path):
|
||||
if os.path.isdir(os.path.join(filesystem_path, filename)):
|
||||
filenames.append(filename + '/')
|
||||
else:
|
||||
filenames.append(filename)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path, filesystem_path)
|
||||
|
||||
# If it's a file
|
||||
elif os.path.isfile(filesystem_path):
|
||||
if self.download_individual_files:
|
||||
return self.stream_individual_file(filesystem_path)
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
|
||||
# If it's not a directory or file, throw a 404
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
else:
|
||||
# Special case loading /
|
||||
|
||||
if path == '':
|
||||
# Root directory listing
|
||||
filenames = list(self.root_files)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path)
|
||||
|
||||
else:
|
||||
# If the path isn't found, throw a 404
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
|
||||
def build_zipfile_list(self, filenames, processed_size_callback=None):
|
||||
self.common.log("ShareModeWeb", "build_zipfile_list")
|
||||
for filename in filenames:
|
||||
info = {
|
||||
'filename': filename,
|
||||
@ -267,33 +289,6 @@ class ShareModeWeb(object):
|
||||
|
||||
return True
|
||||
|
||||
def should_use_gzip(self):
|
||||
"""
|
||||
Should we use gzip for this browser?
|
||||
"""
|
||||
return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
|
||||
|
||||
def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
|
||||
"""
|
||||
Compress a file with gzip, without loading the whole thing into memory
|
||||
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
|
||||
"""
|
||||
bytes_processed = 0
|
||||
blocksize = 1 << 16 # 64kB
|
||||
with open(input_filename, 'rb') as input_file:
|
||||
output_file = gzip.open(output_filename, 'wb', level)
|
||||
while True:
|
||||
if processed_size_callback is not None:
|
||||
processed_size_callback(bytes_processed)
|
||||
|
||||
block = input_file.read(blocksize)
|
||||
if len(block) == 0:
|
||||
break
|
||||
output_file.write(block)
|
||||
bytes_processed += blocksize
|
||||
|
||||
output_file.close()
|
||||
|
||||
|
||||
class ZipWriter(object):
|
||||
"""
|
||||
|
@ -10,7 +10,7 @@ from distutils.version import LooseVersion as Version
|
||||
from urllib.request import urlopen
|
||||
|
||||
import flask
|
||||
from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version
|
||||
from flask import Flask, request, render_template, abort, make_response, send_file, __version__ as flask_version
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
|
||||
from .. import strings
|
||||
@ -30,22 +30,25 @@ except:
|
||||
pass
|
||||
|
||||
|
||||
class Web(object):
|
||||
class Web:
|
||||
"""
|
||||
The Web object is the OnionShare web server, powered by flask
|
||||
"""
|
||||
REQUEST_LOAD = 0
|
||||
REQUEST_STARTED = 1
|
||||
REQUEST_PROGRESS = 2
|
||||
REQUEST_OTHER = 3
|
||||
REQUEST_CANCELED = 4
|
||||
REQUEST_RATE_LIMIT = 5
|
||||
REQUEST_UPLOAD_FILE_RENAMED = 6
|
||||
REQUEST_UPLOAD_SET_DIR = 7
|
||||
REQUEST_UPLOAD_FINISHED = 8
|
||||
REQUEST_UPLOAD_CANCELED = 9
|
||||
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10
|
||||
REQUEST_INVALID_PASSWORD = 11
|
||||
REQUEST_CANCELED = 3
|
||||
REQUEST_RATE_LIMIT = 4
|
||||
REQUEST_UPLOAD_FILE_RENAMED = 5
|
||||
REQUEST_UPLOAD_SET_DIR = 6
|
||||
REQUEST_UPLOAD_FINISHED = 7
|
||||
REQUEST_UPLOAD_CANCELED = 8
|
||||
REQUEST_INDIVIDUAL_FILE_STARTED = 9
|
||||
REQUEST_INDIVIDUAL_FILE_PROGRESS = 10
|
||||
REQUEST_INDIVIDUAL_FILE_CANCELED = 11
|
||||
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12
|
||||
REQUEST_OTHER = 13
|
||||
REQUEST_INVALID_PASSWORD = 14
|
||||
|
||||
def __init__(self, common, is_gui, mode='share'):
|
||||
self.common = common
|
||||
@ -116,13 +119,35 @@ class Web(object):
|
||||
# Create the mode web object, which defines its own routes
|
||||
self.share_mode = None
|
||||
self.receive_mode = None
|
||||
if self.mode == 'receive':
|
||||
self.website_mode = None
|
||||
if self.mode == 'share':
|
||||
self.share_mode = ShareModeWeb(self.common, self)
|
||||
elif self.mode == 'receive':
|
||||
self.receive_mode = ReceiveModeWeb(self.common, self)
|
||||
elif self.mode == 'website':
|
||||
self.website_mode = WebsiteModeWeb(self.common, self)
|
||||
elif self.mode == 'share':
|
||||
self.share_mode = ShareModeWeb(self.common, self)
|
||||
|
||||
def get_mode(self):
|
||||
if self.mode == 'share':
|
||||
return self.share_mode
|
||||
elif self.mode == 'receive':
|
||||
return self.receive_mode
|
||||
elif self.mode == 'website':
|
||||
return self.website_mode
|
||||
else:
|
||||
return None
|
||||
|
||||
def generate_static_url_path(self):
|
||||
# The static URL path has a 128-bit random number in it to avoid having name
|
||||
# collisions with files that might be getting shared
|
||||
self.static_url_path = '/static_{}'.format(self.common.random_string(16))
|
||||
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
|
||||
|
||||
# Update the flask route to handle the new static URL path
|
||||
self.app.static_url_path = self.static_url_path
|
||||
self.app.add_url_rule(
|
||||
self.static_url_path + '/<path:filename>',
|
||||
endpoint='static', view_func=self.app.send_static_file)
|
||||
|
||||
def define_common_routes(self):
|
||||
"""
|
||||
@ -152,7 +177,10 @@ class Web(object):
|
||||
|
||||
@self.app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return self.error404()
|
||||
mode = self.get_mode()
|
||||
history_id = mode.cur_history_id
|
||||
mode.cur_history_id += 1
|
||||
return self.error404(history_id)
|
||||
|
||||
@self.app.route("/<password_candidate>/shutdown")
|
||||
def shutdown(password_candidate):
|
||||
@ -164,6 +192,11 @@ class Web(object):
|
||||
return ""
|
||||
abort(404)
|
||||
|
||||
if self.mode != 'website':
|
||||
@self.app.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_file('{}/img/favicon.ico'.format(self.common.get_resource_path('static')))
|
||||
|
||||
def error401(self):
|
||||
auth = request.authorization
|
||||
if auth:
|
||||
@ -182,15 +215,23 @@ class Web(object):
|
||||
r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error404(self):
|
||||
def error403(self):
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error404(self, history_id):
|
||||
self.add_request(self.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
|
||||
'id': history_id,
|
||||
'status_code': 404
|
||||
})
|
||||
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error403(self):
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
|
||||
r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
|
||||
def error405(self):
|
||||
r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def add_security_headers(self, r):
|
||||
@ -225,18 +266,6 @@ class Web(object):
|
||||
self.password = self.common.build_password()
|
||||
self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
|
||||
|
||||
def generate_static_url_path(self):
|
||||
# The static URL path has a 128-bit random number in it to avoid having name
|
||||
# collisions with files that might be getting shared
|
||||
self.static_url_path = '/static_{}'.format(self.common.random_string(16))
|
||||
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
|
||||
|
||||
# Update the flask route to handle the new static URL path
|
||||
self.app.static_url_path = self.static_url_path
|
||||
self.app.add_url_rule(
|
||||
self.static_url_path + '/<path:filename>',
|
||||
endpoint='static', view_func=self.app.send_static_file)
|
||||
|
||||
def verbose_mode(self):
|
||||
"""
|
||||
Turn on verbose mode, which will log flask errors to a file.
|
||||
|
@ -2,35 +2,23 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import mimetypes
|
||||
from flask import Response, request, render_template, make_response, send_from_directory
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
from .send_base_mode import SendBaseModeWeb
|
||||
from .. import strings
|
||||
|
||||
|
||||
class WebsiteModeWeb(object):
|
||||
class WebsiteModeWeb(SendBaseModeWeb):
|
||||
"""
|
||||
All of the web logic for share mode
|
||||
All of the web logic for website mode
|
||||
"""
|
||||
def __init__(self, common, web):
|
||||
self.common = common
|
||||
self.common.log('WebsiteModeWeb', '__init__')
|
||||
|
||||
self.web = web
|
||||
|
||||
# Dictionary mapping file paths to filenames on disk
|
||||
self.files = {}
|
||||
self.visit_count = 0
|
||||
|
||||
# Reset assets path
|
||||
self.web.app.static_folder=self.common.get_resource_path('static')
|
||||
|
||||
self.define_routes()
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
The web app routes for sharing a website
|
||||
"""
|
||||
|
||||
@self.web.app.route('/', defaults={'path': ''})
|
||||
@self.web.app.route('/<path:path>')
|
||||
def path_public(path):
|
||||
@ -40,142 +28,67 @@ class WebsiteModeWeb(object):
|
||||
"""
|
||||
Render the onionshare website.
|
||||
"""
|
||||
return self.render_logic(path)
|
||||
|
||||
# Each download has a unique id
|
||||
visit_id = self.visit_count
|
||||
self.visit_count += 1
|
||||
|
||||
# Tell GUI the page has been visited
|
||||
self.web.add_request(self.web.REQUEST_STARTED, path, {
|
||||
'id': visit_id,
|
||||
'action': 'visit'
|
||||
})
|
||||
|
||||
if path in self.files:
|
||||
filesystem_path = self.files[path]
|
||||
|
||||
# If it's a directory
|
||||
if os.path.isdir(filesystem_path):
|
||||
# Is there an index.html?
|
||||
index_path = os.path.join(path, 'index.html')
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
dirname = os.path.dirname(self.files[index_path])
|
||||
basename = os.path.basename(self.files[index_path])
|
||||
return send_from_directory(dirname, basename)
|
||||
|
||||
else:
|
||||
# Otherwise, render directory listing
|
||||
filenames = []
|
||||
for filename in os.listdir(filesystem_path):
|
||||
if os.path.isdir(os.path.join(filesystem_path, filename)):
|
||||
filenames.append(filename + '/')
|
||||
else:
|
||||
filenames.append(filename)
|
||||
filenames.sort()
|
||||
return self.directory_listing(path, filenames, filesystem_path)
|
||||
|
||||
# If it's a file
|
||||
elif os.path.isfile(filesystem_path):
|
||||
dirname = os.path.dirname(filesystem_path)
|
||||
basename = os.path.basename(filesystem_path)
|
||||
return send_from_directory(dirname, basename)
|
||||
|
||||
# If it's not a directory or file, throw a 404
|
||||
else:
|
||||
return self.web.error404()
|
||||
else:
|
||||
# Special case loading /
|
||||
if path == '':
|
||||
index_path = 'index.html'
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
dirname = os.path.dirname(self.files[index_path])
|
||||
basename = os.path.basename(self.files[index_path])
|
||||
return send_from_directory(dirname, basename)
|
||||
else:
|
||||
# Root directory listing
|
||||
filenames = list(self.root_files)
|
||||
filenames.sort()
|
||||
return self.directory_listing(path, filenames)
|
||||
|
||||
else:
|
||||
# If the path isn't found, throw a 404
|
||||
return self.web.error404()
|
||||
|
||||
def directory_listing(self, path, filenames, filesystem_path=None):
|
||||
# If filesystem_path is None, this is the root directory listing
|
||||
files = []
|
||||
dirs = []
|
||||
|
||||
for filename in filenames:
|
||||
if filesystem_path:
|
||||
this_filesystem_path = os.path.join(filesystem_path, filename)
|
||||
else:
|
||||
this_filesystem_path = self.files[filename]
|
||||
|
||||
is_dir = os.path.isdir(this_filesystem_path)
|
||||
|
||||
if is_dir:
|
||||
dirs.append({
|
||||
'basename': filename
|
||||
})
|
||||
else:
|
||||
size = os.path.getsize(this_filesystem_path)
|
||||
size_human = self.common.human_readable_filesize(size)
|
||||
files.append({
|
||||
'basename': filename,
|
||||
'size_human': size_human
|
||||
})
|
||||
|
||||
r = make_response(render_template('listing.html',
|
||||
def directory_listing_template(self, path, files, dirs):
|
||||
return make_response(render_template('listing.html',
|
||||
path=path,
|
||||
files=files,
|
||||
dirs=dirs,
|
||||
static_url_path=self.web.static_url_path))
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
def set_file_info(self, filenames):
|
||||
"""
|
||||
Build a data structure that describes the list of files that make up
|
||||
the static website.
|
||||
"""
|
||||
self.common.log("WebsiteModeWeb", "set_file_info")
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
self.common.log("WebsiteModeWeb", "set_file_info_custom")
|
||||
self.web.cancel_compression = True
|
||||
|
||||
# This is a dictionary that maps HTTP routes to filenames on disk
|
||||
self.files = {}
|
||||
def render_logic(self, path=''):
|
||||
if path in self.files:
|
||||
filesystem_path = self.files[path]
|
||||
|
||||
# This is only the root files and dirs, as opposed to all of them
|
||||
self.root_files = {}
|
||||
# If it's a directory
|
||||
if os.path.isdir(filesystem_path):
|
||||
# Is there an index.html?
|
||||
index_path = os.path.join(path, 'index.html')
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
return self.stream_individual_file(filesystem_path)
|
||||
|
||||
# If there's just one folder, replace filenames with a list of files inside that folder
|
||||
if len(filenames) == 1 and os.path.isdir(filenames[0]):
|
||||
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
|
||||
else:
|
||||
# Otherwise, render directory listing
|
||||
filenames = []
|
||||
for filename in os.listdir(filesystem_path):
|
||||
if os.path.isdir(os.path.join(filesystem_path, filename)):
|
||||
filenames.append(filename + '/')
|
||||
else:
|
||||
filenames.append(filename)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path, filesystem_path)
|
||||
|
||||
# Loop through the files
|
||||
for filename in filenames:
|
||||
basename = os.path.basename(filename.rstrip('/'))
|
||||
# If it's a file
|
||||
elif os.path.isfile(filesystem_path):
|
||||
return self.stream_individual_file(filesystem_path)
|
||||
|
||||
# If it's a filename, add it
|
||||
if os.path.isfile(filename):
|
||||
self.files[basename] = filename
|
||||
self.root_files[basename] = filename
|
||||
# If it's not a directory or file, throw a 404
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
else:
|
||||
# Special case loading /
|
||||
|
||||
# If it's a directory, add it recursively
|
||||
elif os.path.isdir(filename):
|
||||
self.root_files[basename + '/'] = filename
|
||||
if path == '':
|
||||
index_path = 'index.html'
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
return self.stream_individual_file(self.files[index_path])
|
||||
else:
|
||||
# Root directory listing
|
||||
filenames = list(self.root_files)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path)
|
||||
|
||||
for root, _, nested_filenames in os.walk(filename):
|
||||
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
|
||||
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
|
||||
# The normalized_root should be "some_folder/foobar"
|
||||
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
|
||||
|
||||
# Add the dir itself
|
||||
self.files[normalized_root + '/'] = root
|
||||
|
||||
# Add the files in this dir
|
||||
for nested_filename in nested_filenames:
|
||||
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
|
||||
|
||||
return True
|
||||
else:
|
||||
# If the path isn't found, throw a 404
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
|
@ -22,6 +22,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
from onionshare import strings
|
||||
from onionshare.common import AutoStopTimer
|
||||
|
||||
from .history import IndividualFileHistoryItem
|
||||
|
||||
from ..server_status import ServerStatus
|
||||
from ..threads import OnionThread
|
||||
from ..threads import AutoStartTimer
|
||||
@ -29,7 +31,7 @@ from ..widgets import Alert
|
||||
|
||||
class Mode(QtWidgets.QWidget):
|
||||
"""
|
||||
The class that ShareMode and ReceiveMode inherit from.
|
||||
The class that all modes inherit from
|
||||
"""
|
||||
start_server_finished = QtCore.pyqtSignal()
|
||||
stop_server_finished = QtCore.pyqtSignal()
|
||||
@ -417,3 +419,32 @@ class Mode(QtWidgets.QWidget):
|
||||
Handle REQUEST_UPLOAD_CANCELED event.
|
||||
"""
|
||||
pass
|
||||
|
||||
def handle_request_individual_file_started(self, event):
|
||||
"""
|
||||
Handle REQUEST_INDVIDIDUAL_FILES_STARTED event.
|
||||
Used in both Share and Website modes, so implemented here.
|
||||
"""
|
||||
self.toggle_history.update_indicator(True)
|
||||
self.history.requests_count += 1
|
||||
self.history.update_requests()
|
||||
|
||||
item = IndividualFileHistoryItem(self.common, event["data"], event["path"])
|
||||
self.history.add(event["data"]["id"], item)
|
||||
|
||||
def handle_request_individual_file_progress(self, event):
|
||||
"""
|
||||
Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event.
|
||||
Used in both Share and Website modes, so implemented here.
|
||||
"""
|
||||
self.history.update(event["data"]["id"], event["data"]["bytes"])
|
||||
|
||||
if self.server_status.status == self.server_status.STATUS_STOPPED:
|
||||
self.history.cancel(event["data"]["id"])
|
||||
|
||||
def handle_request_individual_file_canceled(self, event):
|
||||
"""
|
||||
Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event.
|
||||
Used in both Share and Website modes, so implemented here.
|
||||
"""
|
||||
self.history.cancel(event["data"]["id"])
|
||||
|
@ -237,6 +237,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
|
||||
elif self.common.platform == 'Windows':
|
||||
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
|
||||
|
||||
|
||||
class ReceiveHistoryItem(HistoryItem):
|
||||
def __init__(self, common, id, content_length):
|
||||
super(ReceiveHistoryItem, self).__init__()
|
||||
@ -341,35 +342,108 @@ class ReceiveHistoryItem(HistoryItem):
|
||||
self.label.setText(self.get_canceled_label_text(self.started))
|
||||
|
||||
|
||||
class VisitHistoryItem(HistoryItem):
|
||||
class IndividualFileHistoryItem(HistoryItem):
|
||||
"""
|
||||
Download history item, for share mode
|
||||
Individual file history item, for share mode viewing of individual files
|
||||
"""
|
||||
def __init__(self, common, id, total_bytes):
|
||||
super(VisitHistoryItem, self).__init__()
|
||||
def __init__(self, common, data, path):
|
||||
super(IndividualFileHistoryItem, self).__init__()
|
||||
self.status = HistoryItem.STATUS_STARTED
|
||||
self.common = common
|
||||
|
||||
self.id = id
|
||||
self.visited = time.time()
|
||||
self.visited_dt = datetime.fromtimestamp(self.visited)
|
||||
self.path = path
|
||||
self.total_bytes = 0
|
||||
self.downloaded_bytes = 0
|
||||
self.started = time.time()
|
||||
self.started_dt = datetime.fromtimestamp(self.started)
|
||||
self.status = HistoryItem.STATUS_STARTED
|
||||
|
||||
# Label
|
||||
self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p")))
|
||||
self.directory_listing = 'directory_listing' in data
|
||||
|
||||
# Labels
|
||||
self.timestamp_label = QtWidgets.QLabel(self.started_dt.strftime("%b %d, %I:%M%p"))
|
||||
self.timestamp_label.setStyleSheet(self.common.css['history_individual_file_timestamp_label'])
|
||||
self.path_label = QtWidgets.QLabel("{}".format(self.path))
|
||||
self.status_code_label = QtWidgets.QLabel()
|
||||
|
||||
# Progress bar
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar.setTextVisible(True)
|
||||
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
|
||||
|
||||
# Text layout
|
||||
labels_layout = QtWidgets.QHBoxLayout()
|
||||
labels_layout.addWidget(self.timestamp_label)
|
||||
labels_layout.addWidget(self.path_label)
|
||||
labels_layout.addWidget(self.status_code_label)
|
||||
labels_layout.addStretch()
|
||||
|
||||
# Layout
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addLayout(labels_layout)
|
||||
layout.addWidget(self.progress_bar)
|
||||
self.setLayout(layout)
|
||||
|
||||
def update(self):
|
||||
self.label.setText(self.get_finished_label_text(self.started_dt))
|
||||
self.status = HistoryItem.STATUS_FINISHED
|
||||
# Is a status code already sent?
|
||||
if 'status_code' in data:
|
||||
self.status_code_label.setText("{}".format(data['status_code']))
|
||||
if data['status_code'] >= 200 and data['status_code'] < 300:
|
||||
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
|
||||
if data['status_code'] >= 400 and data['status_code'] < 500:
|
||||
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_4xx'])
|
||||
self.status = HistoryItem.STATUS_FINISHED
|
||||
self.progress_bar.hide()
|
||||
return
|
||||
|
||||
else:
|
||||
self.total_bytes = data['filesize']
|
||||
self.progress_bar.setMinimum(0)
|
||||
self.progress_bar.setMaximum(data['filesize'])
|
||||
self.progress_bar.total_bytes = data['filesize']
|
||||
|
||||
# Start at 0
|
||||
self.update(0)
|
||||
|
||||
def update(self, downloaded_bytes):
|
||||
self.downloaded_bytes = downloaded_bytes
|
||||
|
||||
self.progress_bar.setValue(downloaded_bytes)
|
||||
if downloaded_bytes == self.progress_bar.total_bytes:
|
||||
self.status_code_label.setText("200")
|
||||
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
|
||||
self.progress_bar.hide()
|
||||
self.status = HistoryItem.STATUS_FINISHED
|
||||
|
||||
else:
|
||||
elapsed = time.time() - self.started
|
||||
if elapsed < 10:
|
||||
# Wait a couple of seconds for the download rate to stabilize.
|
||||
# This prevents a "Windows copy dialog"-esque experience at
|
||||
# the beginning of the download.
|
||||
pb_fmt = strings._('gui_all_modes_progress_starting').format(
|
||||
self.common.human_readable_filesize(downloaded_bytes))
|
||||
else:
|
||||
pb_fmt = strings._('gui_all_modes_progress_eta').format(
|
||||
self.common.human_readable_filesize(downloaded_bytes),
|
||||
self.estimated_time_remaining)
|
||||
|
||||
self.progress_bar.setFormat(pb_fmt)
|
||||
|
||||
def cancel(self):
|
||||
self.progress_bar.setFormat(strings._('gui_canceled'))
|
||||
self.status = HistoryItem.STATUS_CANCELED
|
||||
|
||||
@property
|
||||
def estimated_time_remaining(self):
|
||||
return self.common.estimated_time_remaining(self.downloaded_bytes,
|
||||
self.total_bytes,
|
||||
self.started)
|
||||
|
||||
|
||||
class HistoryItemList(QtWidgets.QScrollArea):
|
||||
"""
|
||||
List of items
|
||||
@ -452,26 +526,30 @@ class History(QtWidgets.QWidget):
|
||||
# In progress and completed counters
|
||||
self.in_progress_count = 0
|
||||
self.completed_count = 0
|
||||
self.requests_count = 0
|
||||
|
||||
# In progress and completed labels
|
||||
# In progress, completed, and requests labels
|
||||
self.in_progress_label = QtWidgets.QLabel()
|
||||
self.in_progress_label.setStyleSheet(self.common.css['mode_info_label'])
|
||||
self.completed_label = QtWidgets.QLabel()
|
||||
self.completed_label.setStyleSheet(self.common.css['mode_info_label'])
|
||||
self.requests_label = QtWidgets.QLabel()
|
||||
self.requests_label.setStyleSheet(self.common.css['mode_info_label'])
|
||||
|
||||
# Header
|
||||
self.header_label = QtWidgets.QLabel(header_text)
|
||||
self.header_label.setStyleSheet(self.common.css['downloads_uploads_label'])
|
||||
clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history'))
|
||||
clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
|
||||
clear_button.setFlat(True)
|
||||
clear_button.clicked.connect(self.reset)
|
||||
self.clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history'))
|
||||
self.clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
|
||||
self.clear_button.setFlat(True)
|
||||
self.clear_button.clicked.connect(self.reset)
|
||||
header_layout = QtWidgets.QHBoxLayout()
|
||||
header_layout.addWidget(self.header_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(self.in_progress_label)
|
||||
header_layout.addWidget(self.completed_label)
|
||||
header_layout.addWidget(clear_button)
|
||||
header_layout.addWidget(self.requests_label)
|
||||
header_layout.addWidget(self.clear_button)
|
||||
|
||||
# When there are no items
|
||||
self.empty_image = QtWidgets.QLabel()
|
||||
@ -549,14 +627,18 @@ class History(QtWidgets.QWidget):
|
||||
self.completed_count = 0
|
||||
self.update_completed()
|
||||
|
||||
# Reset web requests counter
|
||||
self.requests_count = 0
|
||||
self.update_requests()
|
||||
|
||||
def update_completed(self):
|
||||
"""
|
||||
Update the 'completed' widget.
|
||||
"""
|
||||
if self.completed_count == 0:
|
||||
image = self.common.get_resource_path('images/share_completed_none.png')
|
||||
image = self.common.get_resource_path('images/history_completed_none.png')
|
||||
else:
|
||||
image = self.common.get_resource_path('images/share_completed.png')
|
||||
image = self.common.get_resource_path('images/history_completed.png')
|
||||
self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count))
|
||||
self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count))
|
||||
|
||||
@ -564,14 +646,25 @@ class History(QtWidgets.QWidget):
|
||||
"""
|
||||
Update the 'in progress' widget.
|
||||
"""
|
||||
if self.mode != 'website':
|
||||
if self.in_progress_count == 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')
|
||||
if self.in_progress_count == 0:
|
||||
image = self.common.get_resource_path('images/history_in_progress_none.png')
|
||||
else:
|
||||
image = self.common.get_resource_path('images/history_in_progress.png')
|
||||
|
||||
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
|
||||
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
|
||||
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
|
||||
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
|
||||
|
||||
def update_requests(self):
|
||||
"""
|
||||
Update the 'web requests' widget.
|
||||
"""
|
||||
if self.requests_count == 0:
|
||||
image = self.common.get_resource_path('images/history_requests_none.png')
|
||||
else:
|
||||
image = self.common.get_resource_path('images/history_requests.png')
|
||||
|
||||
self.requests_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.requests_count))
|
||||
self.requests_label.setToolTip(strings._('history_requests_tooltip').format(self.requests_count))
|
||||
|
||||
|
||||
class ToggleHistory(QtWidgets.QPushButton):
|
||||
@ -604,7 +697,7 @@ class ToggleHistory(QtWidgets.QPushButton):
|
||||
def update_indicator(self, increment=False):
|
||||
"""
|
||||
Update the display of the indicator count. If increment is True, then
|
||||
only increment the counter if Downloads is hidden.
|
||||
only increment the counter if History is hidden.
|
||||
"""
|
||||
if increment and not self.history_widget.isVisible():
|
||||
self.indicator_count += 1
|
||||
|
@ -97,7 +97,7 @@ class ReceiveMode(Mode):
|
||||
The auto-stop timer expired, should we stop the server? Returns a bool
|
||||
"""
|
||||
# If there were no attempts to upload files, or all uploads are done, we can stop
|
||||
if self.web.receive_mode.upload_count == 0 or not self.web.receive_mode.uploads_in_progress:
|
||||
if self.web.receive_mode.cur_history_id == 0 or not self.web.receive_mode.uploads_in_progress:
|
||||
self.server_status.stop_server()
|
||||
self.server_status_label.setText(strings._('close_on_autostop_timer'))
|
||||
return True
|
||||
@ -112,7 +112,7 @@ class ReceiveMode(Mode):
|
||||
Starting the server.
|
||||
"""
|
||||
# Reset web counters
|
||||
self.web.receive_mode.upload_count = 0
|
||||
self.web.receive_mode.cur_history_id = 0
|
||||
self.web.reset_invalid_passwords()
|
||||
|
||||
# Hide and reset the uploads if we have previously shared
|
||||
@ -212,6 +212,8 @@ class ReceiveMode(Mode):
|
||||
Set the info counters back to zero.
|
||||
"""
|
||||
self.history.reset()
|
||||
self.toggle_history.indicator_count = 0
|
||||
self.toggle_history.update_indicator()
|
||||
|
||||
def update_primary_action(self):
|
||||
self.common.log('ReceiveMode', 'update_primary_action')
|
||||
|
@ -132,7 +132,7 @@ class ShareMode(Mode):
|
||||
The auto-stop 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.share_mode.download_count == 0 or self.web.done:
|
||||
if self.web.share_mode.cur_history_id == 0 or self.web.done:
|
||||
self.server_status.stop_server()
|
||||
self.server_status_label.setText(strings._('close_on_autostop_timer'))
|
||||
return True
|
||||
@ -146,7 +146,7 @@ class ShareMode(Mode):
|
||||
Starting the server.
|
||||
"""
|
||||
# Reset web counters
|
||||
self.web.share_mode.download_count = 0
|
||||
self.web.share_mode.cur_history_id = 0
|
||||
self.web.reset_invalid_passwords()
|
||||
|
||||
# Hide and reset the downloads if we have previously shared
|
||||
@ -225,12 +225,6 @@ class ShareMode(Mode):
|
||||
"""
|
||||
self.primary_action.hide()
|
||||
|
||||
def handle_request_load(self, event):
|
||||
"""
|
||||
Handle REQUEST_LOAD event.
|
||||
"""
|
||||
self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_page_loaded_message'))
|
||||
|
||||
def handle_request_started(self, event):
|
||||
"""
|
||||
Handle REQUEST_STARTED event.
|
||||
@ -325,6 +319,8 @@ class ShareMode(Mode):
|
||||
Set the info counters back to zero.
|
||||
"""
|
||||
self.history.reset()
|
||||
self.toggle_history.indicator_count = 0
|
||||
self.toggle_history.update_indicator()
|
||||
|
||||
@staticmethod
|
||||
def _compute_total_size(filenames):
|
||||
|
@ -41,12 +41,8 @@ class CompressThread(QtCore.QThread):
|
||||
self.mode.common.log('CompressThread', 'run')
|
||||
|
||||
try:
|
||||
if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size):
|
||||
self.success.emit()
|
||||
else:
|
||||
# Cancelled
|
||||
pass
|
||||
|
||||
self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size)
|
||||
self.success.emit()
|
||||
self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
|
||||
except OSError as e:
|
||||
self.error.emit(e.strerror)
|
||||
|
@ -30,7 +30,7 @@ from onionshare.web import Web
|
||||
|
||||
from ..file_selection import FileSelection
|
||||
from .. import Mode
|
||||
from ..history import History, ToggleHistory, VisitHistoryItem
|
||||
from ..history import History, ToggleHistory
|
||||
from ...widgets import Alert
|
||||
|
||||
class WebsiteMode(Mode):
|
||||
@ -80,6 +80,8 @@ class WebsiteMode(Mode):
|
||||
strings._('gui_all_modes_history'),
|
||||
'website'
|
||||
)
|
||||
self.history.in_progress_label.hide()
|
||||
self.history.completed_label.hide()
|
||||
self.history.hide()
|
||||
|
||||
# Info label
|
||||
@ -165,12 +167,8 @@ class WebsiteMode(Mode):
|
||||
Step 3 in starting the server. Display large filesize
|
||||
warning, if applicable.
|
||||
"""
|
||||
|
||||
if self.web.website_mode.set_file_info(self.filenames):
|
||||
self.success.emit()
|
||||
else:
|
||||
# Cancelled
|
||||
pass
|
||||
self.web.website_mode.set_file_info(self.filenames)
|
||||
self.success.emit()
|
||||
|
||||
def start_server_error_custom(self):
|
||||
"""
|
||||
@ -208,21 +206,6 @@ class WebsiteMode(Mode):
|
||||
"""
|
||||
self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message'))
|
||||
|
||||
def handle_request_started(self, event):
|
||||
"""
|
||||
Handle REQUEST_STARTED event.
|
||||
"""
|
||||
if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ):
|
||||
item = VisitHistoryItem(self.common, event["data"]["id"], 0)
|
||||
|
||||
self.history.add(event["data"]["id"], item)
|
||||
self.toggle_history.update_indicator(True)
|
||||
self.history.completed_count += 1
|
||||
self.history.update_completed()
|
||||
|
||||
self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message'))
|
||||
|
||||
|
||||
def on_reload_settings(self):
|
||||
"""
|
||||
If there were some files listed for sharing, we should be ok to re-enable
|
||||
@ -262,6 +245,8 @@ class WebsiteMode(Mode):
|
||||
Set the info counters back to zero.
|
||||
"""
|
||||
self.history.reset()
|
||||
self.toggle_history.indicator_count = 0
|
||||
self.toggle_history.update_indicator()
|
||||
|
||||
@staticmethod
|
||||
def _compute_total_size(filenames):
|
||||
|
@ -383,7 +383,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
||||
self.share_mode.server_status.autostart_timer_container.hide()
|
||||
self.receive_mode.server_status.autostart_timer_container.hide()
|
||||
self.website_mode.server_status.autostart_timer_container.hide()
|
||||
|
||||
|
||||
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
|
||||
d.settings_saved.connect(reload_settings)
|
||||
d.exec_()
|
||||
@ -470,6 +470,15 @@ class OnionShareGui(QtWidgets.QMainWindow):
|
||||
elif event["type"] == Web.REQUEST_UPLOAD_CANCELED:
|
||||
mode.handle_request_upload_canceled(event)
|
||||
|
||||
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED:
|
||||
mode.handle_request_individual_file_started(event)
|
||||
|
||||
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS:
|
||||
mode.handle_request_individual_file_progress(event)
|
||||
|
||||
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED:
|
||||
mode.handle_request_individual_file_canceled(event)
|
||||
|
||||
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
|
||||
Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"]))
|
||||
|
||||
|
@ -212,10 +212,12 @@ class SettingsDialog(QtWidgets.QDialog):
|
||||
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"))
|
||||
individual_downloads_label = QtWidgets.QLabel(strings._("gui_settings_individual_downloads_label"))
|
||||
|
||||
# Sharing options layout
|
||||
sharing_group_layout = QtWidgets.QVBoxLayout()
|
||||
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
|
||||
sharing_group_layout.addWidget(individual_downloads_label)
|
||||
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label"))
|
||||
sharing_group.setLayout(sharing_group_layout)
|
||||
|
||||
@ -638,7 +640,6 @@ class SettingsDialog(QtWidgets.QDialog):
|
||||
self.connect_to_tor_label.show()
|
||||
self.onion_settings_widget.hide()
|
||||
|
||||
|
||||
def connection_type_bundled_toggled(self, checked):
|
||||
"""
|
||||
Connection type bundled was toggled. If checked, hide authentication fields.
|
||||
|
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/history_requests.png
Normal file
After Width: | Height: | Size: 738 B |
BIN
share/images/history_requests_none.png
Normal file
After Width: | Height: | Size: 754 B |
@ -52,6 +52,7 @@
|
||||
"gui_settings_onion_label": "Onion settings",
|
||||
"gui_settings_sharing_label": "Sharing settings",
|
||||
"gui_settings_close_after_first_download_option": "Stop sharing after files have been sent",
|
||||
"gui_settings_individual_downloads_label": "Uncheck to allow downloading individual files",
|
||||
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
|
||||
"gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
|
||||
"gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
|
||||
@ -133,6 +134,7 @@
|
||||
"gui_file_info_single": "{} file, {}",
|
||||
"history_in_progress_tooltip": "{} in progress",
|
||||
"history_completed_tooltip": "{} completed",
|
||||
"history_requests_tooltip": "{} web requests",
|
||||
"error_cannot_create_data_dir": "Could not create OnionShare data folder: {}",
|
||||
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.</b>",
|
||||
"gui_mode_share_button": "Share Files",
|
||||
@ -160,6 +162,8 @@
|
||||
"systray_receive_started_message": "Someone is sending files to you",
|
||||
"systray_website_started_title": "Starting sharing website",
|
||||
"systray_website_started_message": "Someone is visiting your website",
|
||||
"systray_individual_file_downloaded_title": "Individual file loaded",
|
||||
"systray_individual_file_downloaded_message": "Individual file {} viewed",
|
||||
"gui_all_modes_history": "History",
|
||||
"gui_all_modes_clear_history": "Clear All",
|
||||
"gui_all_modes_transfer_started": "Started {}",
|
||||
|
@ -56,6 +56,10 @@ header .right ul li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.button:visited {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
color: #ffffff;
|
||||
background-color: #c90c0c;
|
||||
@ -222,3 +226,12 @@ li.info {
|
||||
color: #666666;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1c1ca0;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #601ca0;
|
||||
}
|
19
share/templates/405.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>OnionShare: 405 Method Not Allowed</title>
|
||||
<link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||
<link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="info-wrapper">
|
||||
<div class="info">
|
||||
<p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
|
||||
<p class="info-header">405 Method Not Allowed</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -28,24 +28,31 @@
|
||||
<th id="size-header">Size</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for info in file_info.dirs %}
|
||||
{% for info in dirs %}
|
||||
<tr>
|
||||
<td>
|
||||
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
|
||||
{{ info.basename }}
|
||||
<a href="{{ info.basename }}">
|
||||
{{ info.basename }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ info.size_human }}</td>
|
||||
<td></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for info in file_info.files %}
|
||||
|
||||
{% for info in files %}
|
||||
<tr>
|
||||
<td>
|
||||
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
|
||||
{% if download_individual_files %}
|
||||
<a href="{{ info.basename }}">
|
||||
{{ info.basename }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ info.basename }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ info.size_human }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -14,6 +14,7 @@ from onionshare.web import Web
|
||||
from onionshare_gui import Application, OnionShare, OnionShareGui
|
||||
from onionshare_gui.mode.share_mode import ShareMode
|
||||
from onionshare_gui.mode.receive_mode import ReceiveMode
|
||||
from onionshare_gui.mode.website_mode import WebsiteMode
|
||||
|
||||
|
||||
class GuiBaseTest(object):
|
||||
@ -103,6 +104,9 @@ class GuiBaseTest(object):
|
||||
if type(mode) == ShareMode:
|
||||
QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton)
|
||||
self.assertTrue(self.gui.mode, self.gui.MODE_SHARE)
|
||||
if type(mode) == WebsiteMode:
|
||||
QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton)
|
||||
self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE)
|
||||
|
||||
|
||||
def click_toggle_history(self, mode):
|
||||
@ -112,7 +116,7 @@ class GuiBaseTest(object):
|
||||
self.assertEqual(mode.history.isVisible(), not currently_visible)
|
||||
|
||||
|
||||
def history_indicator(self, mode, public_mode):
|
||||
def history_indicator(self, mode, public_mode, indicator_count="1"):
|
||||
'''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
|
||||
# Make sure history is toggled off
|
||||
if mode.history.isVisible():
|
||||
@ -143,7 +147,7 @@ class GuiBaseTest(object):
|
||||
|
||||
# Indicator should be visible, have a value of "1"
|
||||
self.assertTrue(mode.toggle_history.indicator_label.isVisible())
|
||||
self.assertEqual(mode.toggle_history.indicator_label.text(), "1")
|
||||
self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count)
|
||||
|
||||
# Toggle history back on, indicator should be hidden again
|
||||
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
|
||||
@ -166,6 +170,9 @@ class GuiBaseTest(object):
|
||||
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
|
||||
self.assertEqual(mode.server_status.status, 1)
|
||||
|
||||
def toggle_indicator_is_reset(self, mode):
|
||||
self.assertEqual(mode.toggle_history.indicator_count, 0)
|
||||
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
|
||||
|
||||
def server_status_indicator_says_starting(self, mode):
|
||||
'''Test that the Server Status indicator shows we are Starting'''
|
||||
@ -198,6 +205,9 @@ class GuiBaseTest(object):
|
||||
else:
|
||||
self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)')
|
||||
|
||||
def add_button_visible(self, mode):
|
||||
'''Test that the add button should be visible'''
|
||||
self.assertTrue(mode.server_status.file_selection.add_button.isVisible())
|
||||
|
||||
def url_description_shown(self, mode):
|
||||
'''Test that the URL label is showing'''
|
||||
@ -249,7 +259,7 @@ class GuiBaseTest(object):
|
||||
|
||||
def server_is_stopped(self, mode, stay_open):
|
||||
'''Test that the server stops when we click Stop'''
|
||||
if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open):
|
||||
if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open) or (type(mode) == WebsiteMode):
|
||||
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
|
||||
self.assertEqual(mode.server_status.status, 0)
|
||||
|
||||
@ -275,6 +285,10 @@ class GuiBaseTest(object):
|
||||
else:
|
||||
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically'))
|
||||
|
||||
def clear_all_history_items(self, mode, count):
|
||||
if count == 0:
|
||||
QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton)
|
||||
self.assertEquals(len(mode.history.item_list.items.keys()), count)
|
||||
|
||||
# Auto-stop timer tests
|
||||
def set_timeout(self, mode, timeout):
|
||||
|
@ -66,31 +66,6 @@ class GuiReceiveTest(GuiBaseTest):
|
||||
r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port))
|
||||
self.assertEqual(r.status_code, 401)
|
||||
|
||||
def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode):
|
||||
'''If you submit the receive mode form without selecting any files, the UI shouldn't get updated'''
|
||||
url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
|
||||
|
||||
# What were the counts before submitting the form?
|
||||
before_in_progress_count = mode.history.in_progress_count
|
||||
before_completed_count = mode.history.completed_count
|
||||
before_number_of_history_items = len(mode.history.item_list.items)
|
||||
|
||||
# Click submit without including any files a few times
|
||||
if public_mode:
|
||||
r = requests.post(url, files={})
|
||||
r = requests.post(url, files={})
|
||||
r = requests.post(url, files={})
|
||||
else:
|
||||
auth = requests.auth.HTTPBasicAuth('onionshare', mode.web.password)
|
||||
r = requests.post(url, files={}, auth=auth)
|
||||
r = requests.post(url, files={}, auth=auth)
|
||||
r = requests.post(url, files={}, auth=auth)
|
||||
|
||||
# The counts shouldn't change
|
||||
self.assertEqual(mode.history.in_progress_count, before_in_progress_count)
|
||||
self.assertEqual(mode.history.completed_count, before_completed_count)
|
||||
self.assertEqual(len(mode.history.item_list.items), before_number_of_history_items)
|
||||
|
||||
# 'Grouped' tests follow from here
|
||||
|
||||
def run_all_receive_mode_setup_tests(self, public_mode):
|
||||
@ -127,14 +102,13 @@ class GuiReceiveTest(GuiBaseTest):
|
||||
# Test uploading the same file twice at the same time, and make sure no collisions
|
||||
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt', True)
|
||||
self.counter_incremented(self.gui.receive_mode, 6)
|
||||
self.uploading_zero_files_shouldnt_change_ui(self.gui.receive_mode, public_mode)
|
||||
self.history_indicator(self.gui.receive_mode, public_mode)
|
||||
self.history_indicator(self.gui.receive_mode, public_mode, "2")
|
||||
self.server_is_stopped(self.gui.receive_mode, False)
|
||||
self.web_server_is_stopped()
|
||||
self.server_status_indicator_says_closed(self.gui.receive_mode, False)
|
||||
self.server_working_on_start_button_pressed(self.gui.receive_mode)
|
||||
self.server_is_started(self.gui.receive_mode)
|
||||
self.history_indicator(self.gui.receive_mode, public_mode)
|
||||
self.history_indicator(self.gui.receive_mode, public_mode, "2")
|
||||
|
||||
def run_all_receive_mode_unwritable_dir_tests(self, public_mode):
|
||||
'''Attempt to upload (unwritable) files in receive mode and stop the share'''
|
||||
@ -153,3 +127,12 @@ class GuiReceiveTest(GuiBaseTest):
|
||||
self.autostop_timer_widget_hidden(self.gui.receive_mode)
|
||||
self.server_timed_out(self.gui.receive_mode, 15000)
|
||||
self.web_server_is_stopped()
|
||||
|
||||
def run_all_clear_all_button_tests(self, public_mode):
|
||||
"""Test the Clear All history button"""
|
||||
self.run_all_receive_mode_setup_tests(public_mode)
|
||||
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
|
||||
self.history_widgets_present(self.gui.receive_mode)
|
||||
self.clear_all_history_items(self.gui.receive_mode, 0)
|
||||
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
|
||||
self.clear_all_history_items(self.gui.receive_mode, 2)
|
||||
|
@ -44,7 +44,7 @@ class GuiShareTest(GuiBaseTest):
|
||||
self.file_selection_widget_has_files(0)
|
||||
|
||||
|
||||
def file_selection_widget_readd_files(self):
|
||||
def file_selection_widget_read_files(self):
|
||||
'''Re-add some files to the list so we can share'''
|
||||
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
|
||||
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt')
|
||||
@ -81,6 +81,40 @@ class GuiShareTest(GuiBaseTest):
|
||||
QtTest.QTest.qWait(2000)
|
||||
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
|
||||
|
||||
def individual_file_is_viewable_or_not(self, public_mode, stay_open):
|
||||
'''Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)'''
|
||||
url = "http://127.0.0.1:{}".format(self.gui.app.port)
|
||||
download_file_url = "http://127.0.0.1:{}/test.txt".format(self.gui.app.port)
|
||||
if public_mode:
|
||||
r = requests.get(url)
|
||||
else:
|
||||
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
|
||||
|
||||
if stay_open:
|
||||
self.assertTrue('a href="test.txt"' in r.text)
|
||||
|
||||
if public_mode:
|
||||
r = requests.get(download_file_url)
|
||||
else:
|
||||
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
|
||||
|
||||
tmp_file = tempfile.NamedTemporaryFile()
|
||||
with open(tmp_file.name, 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
with open(tmp_file.name, 'r') as f:
|
||||
self.assertEqual('onionshare', f.read())
|
||||
else:
|
||||
self.assertFalse('a href="/test.txt"' in r.text)
|
||||
if public_mode:
|
||||
r = requests.get(download_file_url)
|
||||
else:
|
||||
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
|
||||
self.assertEqual(r.status_code, 404)
|
||||
self.download_share(public_mode)
|
||||
|
||||
QtTest.QTest.qWait(2000)
|
||||
|
||||
def hit_401(self, public_mode):
|
||||
'''Test that the server stops after too many 401s, or doesn't when in public_mode'''
|
||||
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
|
||||
@ -101,11 +135,6 @@ class GuiShareTest(GuiBaseTest):
|
||||
self.web_server_is_stopped()
|
||||
|
||||
|
||||
def add_button_visible(self):
|
||||
'''Test that the add button should be visible'''
|
||||
self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
|
||||
|
||||
|
||||
# 'Grouped' tests follow from here
|
||||
|
||||
def run_all_share_mode_setup_tests(self):
|
||||
@ -117,7 +146,7 @@ class GuiShareTest(GuiBaseTest):
|
||||
self.history_is_visible(self.gui.share_mode)
|
||||
self.deleting_all_files_hides_delete_button()
|
||||
self.add_a_file_and_delete_using_its_delete_widget()
|
||||
self.file_selection_widget_readd_files()
|
||||
self.file_selection_widget_read_files()
|
||||
|
||||
|
||||
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
|
||||
@ -142,11 +171,24 @@ class GuiShareTest(GuiBaseTest):
|
||||
self.server_is_stopped(self.gui.share_mode, stay_open)
|
||||
self.web_server_is_stopped()
|
||||
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
|
||||
self.add_button_visible()
|
||||
self.add_button_visible(self.gui.share_mode)
|
||||
self.server_working_on_start_button_pressed(self.gui.share_mode)
|
||||
self.toggle_indicator_is_reset(self.gui.share_mode)
|
||||
self.server_is_started(self.gui.share_mode)
|
||||
self.history_indicator(self.gui.share_mode, public_mode)
|
||||
|
||||
def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open):
|
||||
"""Tests in share mode after downloading a share"""
|
||||
self.web_page(self.gui.share_mode, 'Total size', public_mode)
|
||||
self.individual_file_is_viewable_or_not(public_mode, stay_open)
|
||||
self.history_widgets_present(self.gui.share_mode)
|
||||
self.server_is_stopped(self.gui.share_mode, stay_open)
|
||||
self.web_server_is_stopped()
|
||||
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
|
||||
self.add_button_visible(self.gui.share_mode)
|
||||
self.server_working_on_start_button_pressed(self.gui.share_mode)
|
||||
self.server_is_started(self.gui.share_mode)
|
||||
self.history_indicator(self.gui.share_mode, public_mode)
|
||||
|
||||
def run_all_share_mode_tests(self, public_mode, stay_open):
|
||||
"""End-to-end share tests"""
|
||||
@ -154,6 +196,21 @@ class GuiShareTest(GuiBaseTest):
|
||||
self.run_all_share_mode_started_tests(public_mode)
|
||||
self.run_all_share_mode_download_tests(public_mode, stay_open)
|
||||
|
||||
def run_all_clear_all_button_tests(self, public_mode, stay_open):
|
||||
"""Test the Clear All history button"""
|
||||
self.run_all_share_mode_setup_tests()
|
||||
self.run_all_share_mode_started_tests(public_mode)
|
||||
self.individual_file_is_viewable_or_not(public_mode, stay_open)
|
||||
self.history_widgets_present(self.gui.share_mode)
|
||||
self.clear_all_history_items(self.gui.share_mode, 0)
|
||||
self.individual_file_is_viewable_or_not(public_mode, stay_open)
|
||||
self.clear_all_history_items(self.gui.share_mode, 2)
|
||||
|
||||
def run_all_share_mode_individual_file_tests(self, public_mode, stay_open):
|
||||
"""Tests in share mode when viewing an individual file"""
|
||||
self.run_all_share_mode_setup_tests()
|
||||
self.run_all_share_mode_started_tests(public_mode)
|
||||
self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open)
|
||||
|
||||
def run_all_large_file_tests(self, public_mode, stay_open):
|
||||
"""Same as above but with a larger file"""
|
||||
|
100
tests/GuiWebsiteTest.py
Normal file
@ -0,0 +1,100 @@
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import socks
|
||||
import zipfile
|
||||
import tempfile
|
||||
from PyQt5 import QtCore, QtTest
|
||||
from onionshare import strings
|
||||
from onionshare.common import Common
|
||||
from onionshare.settings import Settings
|
||||
from onionshare.onion import Onion
|
||||
from onionshare.web import Web
|
||||
from onionshare_gui import Application, OnionShare, OnionShareGui
|
||||
from .GuiShareTest import GuiShareTest
|
||||
|
||||
class GuiWebsiteTest(GuiShareTest):
|
||||
@staticmethod
|
||||
def set_up(test_settings):
|
||||
'''Create GUI with given settings'''
|
||||
# Create our test file
|
||||
testfile = open('/tmp/index.html', 'w')
|
||||
testfile.write('<html><body><p>This is a test website hosted by OnionShare</p></body></html>')
|
||||
testfile.close()
|
||||
|
||||
common = Common()
|
||||
common.settings = Settings(common)
|
||||
common.define_css()
|
||||
strings.load_strings(common)
|
||||
|
||||
# Get all of the settings in test_settings
|
||||
test_settings['data_dir'] = '/tmp/OnionShare'
|
||||
for key, val in common.settings.default_settings.items():
|
||||
if key not in test_settings:
|
||||
test_settings[key] = val
|
||||
|
||||
# Start the Onion
|
||||
testonion = Onion(common)
|
||||
global qtapp
|
||||
qtapp = Application(common)
|
||||
app = OnionShare(common, testonion, True, 0)
|
||||
|
||||
web = Web(common, False, True)
|
||||
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
|
||||
|
||||
gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/index.html'], '/tmp/settings.json', True)
|
||||
return gui
|
||||
|
||||
@staticmethod
|
||||
def tear_down():
|
||||
'''Clean up after tests'''
|
||||
try:
|
||||
os.remove('/tmp/index.html')
|
||||
os.remove('/tmp/settings.json')
|
||||
except:
|
||||
pass
|
||||
|
||||
def view_website(self, public_mode):
|
||||
'''Test that we can download the share'''
|
||||
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
|
||||
if public_mode:
|
||||
r = requests.get(url)
|
||||
else:
|
||||
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password))
|
||||
|
||||
QtTest.QTest.qWait(2000)
|
||||
self.assertTrue('This is a test website hosted by OnionShare' in r.text)
|
||||
|
||||
def run_all_website_mode_setup_tests(self):
|
||||
"""Tests in website mode prior to starting a share"""
|
||||
self.click_mode(self.gui.website_mode)
|
||||
self.file_selection_widget_has_files(1)
|
||||
self.history_is_not_visible(self.gui.website_mode)
|
||||
self.click_toggle_history(self.gui.website_mode)
|
||||
self.history_is_visible(self.gui.website_mode)
|
||||
|
||||
def run_all_website_mode_started_tests(self, public_mode, startup_time=2000):
|
||||
"""Tests in website mode after starting a share"""
|
||||
self.server_working_on_start_button_pressed(self.gui.website_mode)
|
||||
self.server_status_indicator_says_starting(self.gui.website_mode)
|
||||
self.add_delete_buttons_hidden()
|
||||
self.settings_button_is_hidden()
|
||||
self.server_is_started(self.gui.website_mode, startup_time)
|
||||
self.web_server_is_running()
|
||||
self.have_a_password(self.gui.website_mode, public_mode)
|
||||
self.url_description_shown(self.gui.website_mode)
|
||||
self.have_copy_url_button(self.gui.website_mode, public_mode)
|
||||
self.server_status_indicator_says_started(self.gui.website_mode)
|
||||
|
||||
|
||||
def run_all_website_mode_download_tests(self, public_mode):
|
||||
"""Tests in website mode after viewing the site"""
|
||||
self.run_all_website_mode_setup_tests()
|
||||
self.run_all_website_mode_started_tests(public_mode, startup_time=2000)
|
||||
self.view_website(public_mode)
|
||||
self.history_widgets_present(self.gui.website_mode)
|
||||
self.server_is_stopped(self.gui.website_mode, False)
|
||||
self.web_server_is_stopped()
|
||||
self.server_status_indicator_says_closed(self.gui.website_mode, False)
|
||||
self.add_button_visible(self.gui.website_mode)
|
||||
|
@ -67,7 +67,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
|
||||
self.server_is_stopped(self.gui.share_mode, stay_open)
|
||||
self.web_server_is_stopped()
|
||||
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
|
||||
self.add_button_visible()
|
||||
self.add_button_visible(self.gui.share_mode)
|
||||
self.server_working_on_start_button_pressed(self.gui.share_mode)
|
||||
self.server_is_started(self.gui.share_mode, startup_time=45000)
|
||||
self.history_indicator(self.gui.share_mode, public_mode)
|
||||
|
25
tests/local_onionshare_receive_mode_clear_all_button_test.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from .GuiReceiveTest import GuiReceiveTest
|
||||
|
||||
class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
test_settings = {
|
||||
}
|
||||
cls.gui = GuiReceiveTest.set_up(test_settings)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
GuiReceiveTest.tear_down()
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
|
||||
def test_gui(self):
|
||||
self.run_all_common_setup_tests()
|
||||
self.run_all_clear_all_button_tests(False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
26
tests/local_onionshare_share_mode_clear_all_button_test.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from .GuiShareTest import GuiShareTest
|
||||
|
||||
class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
test_settings = {
|
||||
"close_after_first_download": False,
|
||||
}
|
||||
cls.gui = GuiShareTest.set_up(test_settings)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
GuiShareTest.tear_down()
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
|
||||
def test_gui(self):
|
||||
self.run_all_common_setup_tests()
|
||||
self.run_all_clear_all_button_tests(False, True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from .GuiShareTest import GuiShareTest
|
||||
|
||||
class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
test_settings = {
|
||||
"close_after_first_download": False,
|
||||
}
|
||||
cls.gui = GuiShareTest.set_up(test_settings)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
GuiShareTest.tear_down()
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
|
||||
def test_gui(self):
|
||||
self.run_all_common_setup_tests()
|
||||
self.run_all_share_mode_individual_file_tests(False, True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from .GuiShareTest import GuiShareTest
|
||||
|
||||
class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
test_settings = {
|
||||
"close_after_first_download": True,
|
||||
}
|
||||
cls.gui = GuiShareTest.set_up(test_settings)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
GuiShareTest.tear_down()
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
|
||||
def test_gui(self):
|
||||
self.run_all_common_setup_tests()
|
||||
self.run_all_share_mode_individual_file_tests(False, False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
25
tests/local_onionshare_website_mode_test.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from .GuiWebsiteTest import GuiWebsiteTest
|
||||
|
||||
class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
test_settings = {
|
||||
}
|
||||
cls.gui = GuiWebsiteTest.set_up(test_settings)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
GuiWebsiteTest.tear_down()
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
|
||||
def test_gui(self):
|
||||
#self.run_all_common_setup_tests()
|
||||
self.run_all_website_mode_download_tests(False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|