mirror of
https://github.com/onionshare/onionshare.git
synced 2024-10-01 01:35:40 -04:00
support for multiple files and folders (#66)
This commit is contained in:
parent
e34a88b112
commit
c5ced60f8b
@ -1,4 +1,4 @@
|
||||
import os, inspect, hashlib, base64, hmac, platform
|
||||
import os, inspect, hashlib, base64, hmac, platform, zipfile
|
||||
from itertools import izip
|
||||
|
||||
def get_platform():
|
||||
@ -30,10 +30,13 @@ def constant_time_compare(val1, val2):
|
||||
result |= x ^ y
|
||||
return result == 0
|
||||
|
||||
def random_string(num_bytes):
|
||||
def random_string(num_bytes, output_len=None):
|
||||
b = os.urandom(num_bytes)
|
||||
h = hashlib.sha256(b).digest()[:16]
|
||||
return base64.b32encode(h).lower().replace('=','')
|
||||
s = base64.b32encode(h).lower().replace('=','')
|
||||
if not output_len:
|
||||
return s
|
||||
return s[:output_len]
|
||||
|
||||
def human_readable_filesize(b):
|
||||
thresh = 1024.0
|
||||
@ -50,17 +53,46 @@ def human_readable_filesize(b):
|
||||
def is_root():
|
||||
return os.geteuid() == 0
|
||||
|
||||
def file_crunching(filename):
|
||||
# calculate filehash, file size
|
||||
BLOCKSIZE = 65536
|
||||
hasher = hashlib.sha1()
|
||||
with open(filename, 'rb') as f:
|
||||
buf = f.read(BLOCKSIZE)
|
||||
while len(buf) > 0:
|
||||
hasher.update(buf)
|
||||
buf = f.read(BLOCKSIZE)
|
||||
filehash = hasher.hexdigest()
|
||||
filesize = os.path.getsize(filename)
|
||||
return filehash, filesize
|
||||
def dir_size(start_path):
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(start_path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
if not os.path.islink(fp):
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
||||
|
||||
def get_tmp_dir():
|
||||
if get_platform() == "Windows":
|
||||
if 'Temp' in os.environ:
|
||||
temp = os.environ['Temp'].replace('\\', '/')
|
||||
else:
|
||||
temp = 'C:/tmp'
|
||||
else:
|
||||
temp = '/tmp'
|
||||
return temp
|
||||
|
||||
class ZipWriter(object):
|
||||
def __init__(self, zip_filename=None):
|
||||
if zip_filename:
|
||||
self.zip_filename = zip_filename
|
||||
else:
|
||||
self.zip_filename = '{0}/onionshare_{1}.zip'.format(get_tmp_dir(), random_string(4, 6))
|
||||
|
||||
self.z = zipfile.ZipFile(self.zip_filename, 'w')
|
||||
|
||||
def add_file(self, filename):
|
||||
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
|
||||
|
||||
def add_dir(self, filename):
|
||||
dir_to_strip = os.path.dirname(filename)+'/'
|
||||
for dirpath, dirnames, filenames in os.walk(filename):
|
||||
for f in filenames:
|
||||
full_filename = os.path.join(dirpath, f)
|
||||
if not os.path.islink(full_filename):
|
||||
arc_filename = full_filename[len(dir_to_strip):]
|
||||
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
|
||||
|
||||
def close(self):
|
||||
self.z.close()
|
||||
|
||||
|
@ -1,76 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OnionShare</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #222222;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
font-family: arial;
|
||||
padding: 5em 1em;
|
||||
}
|
||||
.metadata {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
color: #999999;
|
||||
text-align: left;
|
||||
}
|
||||
.button {
|
||||
-moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
-webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #79bbff), color-stop(1, #4197ee) );
|
||||
background:-moz-linear-gradient( center top, #79bbff 5%, #4197ee 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#79bbff', endColorstr='#4197ee');
|
||||
background-color:#79bbff;
|
||||
-webkit-border-top-left-radius:12px;
|
||||
-moz-border-radius-topleft:12px;
|
||||
border-top-left-radius:12px;
|
||||
-webkit-border-top-right-radius:12px;
|
||||
-moz-border-radius-topright:12px;
|
||||
border-top-right-radius:12px;
|
||||
-webkit-border-bottom-right-radius:12px;
|
||||
-moz-border-radius-bottomright:12px;
|
||||
border-bottom-right-radius:12px;
|
||||
-webkit-border-bottom-left-radius:12px;
|
||||
-moz-border-radius-bottomleft:12px;
|
||||
border-bottom-left-radius:12px;
|
||||
text-indent:0;
|
||||
border:1px solid #469df5;
|
||||
display:inline-block;
|
||||
color:#ffffff;
|
||||
font-family:Arial;
|
||||
font-size:29px;
|
||||
font-weight:bold;
|
||||
font-style:normal;
|
||||
height:50px;
|
||||
line-height:50px;
|
||||
text-decoration:none;
|
||||
text-align:center;
|
||||
text-shadow:1px 1px 0px #287ace;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.button:hover {
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #4197ee), color-stop(1, #79bbff) );
|
||||
background:-moz-linear-gradient( center top, #4197ee 5%, #79bbff 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4197ee', endColorstr='#79bbff');
|
||||
background-color:#4197ee;
|
||||
}.button:active {
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
</style>
|
||||
<meta name="onionshare-filename" content="{{ filename }}">
|
||||
<meta name="onionshare-filesize" content="{{ filesize }}">
|
||||
<meta name="onionshare-filehash" content="{{ filehash }}">
|
||||
</head>
|
||||
<body>
|
||||
<p><a class="button" href='/{{ slug }}/download'>{{ filename }} ▼</a></p>
|
||||
<head>
|
||||
<title>OnionShare</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #222222;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
padding: 5em 1em;
|
||||
}
|
||||
.button {
|
||||
-moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
-webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #79bbff), color-stop(1, #4197ee) );
|
||||
background:-moz-linear-gradient( center top, #79bbff 5%, #4197ee 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#79bbff', endColorstr='#4197ee');
|
||||
background-color:#79bbff;
|
||||
-webkit-border-top-left-radius:12px;
|
||||
-moz-border-radius-topleft:12px;
|
||||
border-top-left-radius:12px;
|
||||
-webkit-border-top-right-radius:12px;
|
||||
-moz-border-radius-topright:12px;
|
||||
border-top-right-radius:12px;
|
||||
-webkit-border-bottom-right-radius:12px;
|
||||
-moz-border-radius-bottomright:12px;
|
||||
border-bottom-right-radius:12px;
|
||||
-webkit-border-bottom-left-radius:12px;
|
||||
-moz-border-radius-bottomleft:12px;
|
||||
border-bottom-left-radius:12px;
|
||||
text-indent:0;
|
||||
border:1px solid #469df5;
|
||||
display:inline-block;
|
||||
color:#ffffff;
|
||||
font-size:29px;
|
||||
font-weight:bold;
|
||||
font-style:normal;
|
||||
height:50px;
|
||||
line-height:50px;
|
||||
text-decoration:none;
|
||||
text-align:center;
|
||||
text-shadow:1px 1px 0px #287ace;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.button:hover {
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #4197ee), color-stop(1, #79bbff) );
|
||||
background:-moz-linear-gradient( center top, #4197ee 5%, #79bbff 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4197ee', endColorstr='#79bbff');
|
||||
background-color:#4197ee;
|
||||
}.button:active {
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
|
||||
<div class="metadata">
|
||||
<p>{{strings.filesize}}: <strong title="{{ filesize }} bytes">{{ filesize_human }}</strong></p>
|
||||
<p>{{strings.sha1_checksum}}: <strong>{{ filehash }}</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
.download-size {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin: 50px auto 0 auto;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
background-color: #333333;
|
||||
}
|
||||
.file-list th {
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.file-list td {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
<meta name="onionshare-filename" content="{{ filename }}">
|
||||
<meta name="onionshare-filesize" content="{{ filesize }}">
|
||||
</head>
|
||||
<body>
|
||||
<p><a class="button" href='/{{ slug }}/download'>{{ filename }} ▼</a></p>
|
||||
<p class="download-size">{{strings.download_size}}: <strong title="{{ filesize }} bytes">{{ filesize_human }}</strong></p>
|
||||
<table class="file-list">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{strings.filename}}</th>
|
||||
<th>{{strings.size}}</th>
|
||||
</tr>
|
||||
{% for info in file_info.dirs %}
|
||||
<tr>
|
||||
<td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQXOpwjbQAAAM5JREFUSMftlksOwyAMRDFCbNjkDL1kz9K75Cq9AZF3kYIM7oqoP/KpcFK1mRUrnsbW2Fbq0N+JiPhZRMS1OZAfzLzocwCAGmC9V2Vhjduarj8CzynGqIwxsDl4SXV267E4uFTN33X8deCxDyV1XXeRYJo1UUDEa9M0pxoRK+b4HiqRcz3nVAJKRK+TSxqaGbrkNKUkBn0oNSK2+T0MA1dau9NzO4QwuvPen1lAxQtEsq/vNpSWhvZ9v/3IRMTWOQezkyuEoKy14tfHoU11A6Mr5AxrpuMVAAAAAElFTkSuQmCC" /></td>
|
||||
<td>{{ info.basename }}</td>
|
||||
<td>{{ info.size_human }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for info in file_info.files %}
|
||||
<tr>
|
||||
<td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQqYvRvfAAAAJFJREFUSMftlUkOwCAIAEWJ//+uIUivtYmtG7VN8KYHRhlA5zYtuB6klCTG2BcEAHrBeN4ws3jvX3lxQZmFiogMgVesVrhKXlvgakKf4KqVdAdXL+Ea3O8aILh0GnUMEhjtw9mLbUv1tx33fgItynBVoN+k2hybY3Nsjg08Bs45q4GYuQ4OIQARLYcSkUPEoiUP4j5IFasKOnUAAAAASUVORK5CYII=" /></td>
|
||||
<td>{{ info.basename }}</td>
|
||||
<td>{{ info.size_human }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<table>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -21,16 +21,19 @@ class OnionShare(object):
|
||||
# automatically close when download is finished
|
||||
self.stay_open = stay_open
|
||||
|
||||
# list of hidden service dirs to cleanup
|
||||
self.hidserv_dirs = []
|
||||
# files and dirs to delete on shutdown
|
||||
self.cleanup_filenames = []
|
||||
|
||||
# choose a random port
|
||||
self.choose_port()
|
||||
self.local_host = "127.0.0.1:{0}".format(self.port)
|
||||
|
||||
def cleanup(self):
|
||||
for d in self.hidserv_dirs:
|
||||
shutil.rmtree(d)
|
||||
for filename in self.cleanup_filenames:
|
||||
if os.path.isfile(filename):
|
||||
os.remove(filename)
|
||||
elif os.path.isdir(filename):
|
||||
shutil.rmtree(filename)
|
||||
|
||||
def choose_port(self):
|
||||
# let the OS choose a port
|
||||
@ -65,17 +68,8 @@ class OnionShare(object):
|
||||
print strings._("connecting_ctrlport").format(self.port)
|
||||
|
||||
# come up with a hidden service directory name
|
||||
hidserv_dir_rand = helpers.random_string(8)
|
||||
if helpers.get_platform() == "Windows":
|
||||
if 'Temp' in os.environ:
|
||||
temp = os.environ['Temp'].replace('\\', '/')
|
||||
else:
|
||||
temp = 'C:/tmp'
|
||||
hidserv_dir = "{0}/onionshare_{1}".format(temp, hidserv_dir_rand)
|
||||
else:
|
||||
hidserv_dir = "/tmp/onionshare_{0}".format(hidserv_dir_rand)
|
||||
|
||||
self.hidserv_dirs.append(hidserv_dir)
|
||||
hidserv_dir = '{0}/onionshare_{1}'.format(helpers.get_tmp_dir(), helpers.random_string(8))
|
||||
self.cleanup_filenames.append(hidserv_dir)
|
||||
|
||||
# connect to the tor controlport
|
||||
controlports = [9051, 9151]
|
||||
@ -141,17 +135,25 @@ def main():
|
||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||
parser.add_argument('filename', nargs=1, help=strings._("help_filename"))
|
||||
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
|
||||
args = parser.parse_args()
|
||||
|
||||
filename = os.path.abspath(args.filename[0])
|
||||
filenames = args.filename
|
||||
for i in range(len(filenames)):
|
||||
filenames[i] = os.path.abspath(filenames[i])
|
||||
|
||||
local_only = bool(args.local_only)
|
||||
debug = bool(args.debug)
|
||||
stay_open = bool(args.stay_open)
|
||||
|
||||
if not (filename and os.path.isfile(filename)):
|
||||
sys.exit(strings._("not_a_file").format(filename))
|
||||
filename = os.path.abspath(filename)
|
||||
# validation
|
||||
valid = True
|
||||
for filename in filenames:
|
||||
if not os.path.exists(filename):
|
||||
print(strings._("not_a_file").format(filename))
|
||||
valid = False
|
||||
if not valid:
|
||||
sys.exit()
|
||||
|
||||
# start the onionshare app
|
||||
try:
|
||||
@ -163,10 +165,9 @@ def main():
|
||||
sys.exit(e.args[0])
|
||||
|
||||
# startup
|
||||
print strings._("calculating_sha1")
|
||||
filehash, filesize = helpers.file_crunching(filename)
|
||||
web.set_file_info(filename, filehash, filesize)
|
||||
print '\n' + strings._("give_this_url")
|
||||
web.set_file_info(filenames)
|
||||
app.cleanup_filenames.append(web.zip_filename)
|
||||
print strings._("give_this_url")
|
||||
print 'http://{0}/{1}'.format(app.onion_host, web.slug)
|
||||
print ''
|
||||
print strings._("ctrlc_to_stop")
|
||||
|
@ -5,7 +5,9 @@
|
||||
"give_this_url": "Give this URL to the person you're sending the file to:",
|
||||
"ctrlc_to_stop": "Press Ctrl-C to stop server",
|
||||
"not_a_file": "{0} is not a file.",
|
||||
"filesize": "File size",
|
||||
"download_size": "Download size",
|
||||
"filename": "Filename",
|
||||
"size": "Size",
|
||||
"sha1_checksum": "SHA1 checksum",
|
||||
"copied_url": "Copied URL to clipboard",
|
||||
"download_page_loaded": "Download page loaded",
|
||||
@ -24,7 +26,7 @@
|
||||
"help_local_only": "Do not attempt to use tor: for development only",
|
||||
"help_stay_open": "Keep hidden service running after download has finished",
|
||||
"help_debug": "Log errors to disk",
|
||||
"help_filename": "File to share"
|
||||
"help_filename": "List of files or folders to share"
|
||||
}, "no": {
|
||||
"calculating_sha1": "Kalkulerer SHA1 sjekksum.",
|
||||
"connecting_ctrlport": "Kobler til Tors kontroll-port for å sette opp en gjemt tjeneste på port {0}.",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Queue, mimetypes, platform, os, sys
|
||||
import Queue, mimetypes, platform, os, sys, zipfile
|
||||
from flask import Flask, Response, request, render_template_string, abort
|
||||
|
||||
import strings, helpers
|
||||
@ -6,12 +6,37 @@ import strings, helpers
|
||||
app = Flask(__name__)
|
||||
|
||||
# information about the file
|
||||
filename = filesize = filehash = None
|
||||
def set_file_info(new_filename, new_filehash, new_filesize):
|
||||
global filename, filehash, filesize
|
||||
filename = new_filename
|
||||
filehash = new_filehash
|
||||
filesize = new_filesize
|
||||
file_info = []
|
||||
zip_filename = None
|
||||
zip_filesize = None
|
||||
def set_file_info(filenames):
|
||||
global file_info, zip_filename, zip_filesize
|
||||
|
||||
# build file info list
|
||||
file_info = {'files':[], 'dirs':[]}
|
||||
for filename in filenames:
|
||||
info = {
|
||||
'filename': filename,
|
||||
'basename': os.path.basename(filename)
|
||||
}
|
||||
if os.path.isfile(filename):
|
||||
info['size'] = os.path.getsize(filename)
|
||||
info['size_human'] = helpers.human_readable_filesize(info['size'])
|
||||
file_info['files'].append(info)
|
||||
if os.path.isdir(filename):
|
||||
info['size'] = helpers.dir_size(filename)
|
||||
info['size_human'] = helpers.human_readable_filesize(info['size'])
|
||||
file_info['dirs'].append(info)
|
||||
|
||||
# zip up the files and folders
|
||||
z = helpers.ZipWriter()
|
||||
for info in file_info['files']:
|
||||
z.add_file(info['filename'])
|
||||
for info in file_info['dirs']:
|
||||
z.add_dir(info['filename'])
|
||||
z.close()
|
||||
zip_filename = z.zip_filename
|
||||
zip_filesize = os.path.getsize(zip_filename)
|
||||
|
||||
REQUEST_LOAD = 0
|
||||
REQUEST_DOWNLOAD = 1
|
||||
@ -58,10 +83,10 @@ def index(slug_candidate):
|
||||
return render_template_string(
|
||||
open('{0}/index.html'.format(helpers.get_onionshare_dir())).read(),
|
||||
slug=slug,
|
||||
filename=os.path.basename(filename).decode("utf-8"),
|
||||
filehash=filehash,
|
||||
filesize=filesize,
|
||||
filesize_human=helpers.human_readable_filesize(filesize),
|
||||
file_info=file_info,
|
||||
filename=os.path.basename(zip_filename).decode("utf-8"),
|
||||
filesize=zip_filesize,
|
||||
filesize_human=helpers.human_readable_filesize(zip_filesize),
|
||||
strings=strings.strings
|
||||
)
|
||||
|
||||
@ -83,13 +108,13 @@ def download(slug_candidate):
|
||||
# tell GUI the download started
|
||||
add_request(REQUEST_DOWNLOAD, path, { 'id':download_id })
|
||||
|
||||
dirname = os.path.dirname(filename)
|
||||
basename = os.path.basename(filename)
|
||||
dirname = os.path.dirname(zip_filename)
|
||||
basename = os.path.basename(zip_filename)
|
||||
|
||||
def generate():
|
||||
chunk_size = 102400 # 100kb
|
||||
|
||||
fp = open(filename, 'rb')
|
||||
fp = open(zip_filename, 'rb')
|
||||
done = False
|
||||
while not done:
|
||||
chunk = fp.read(102400)
|
||||
@ -100,7 +125,7 @@ def download(slug_candidate):
|
||||
|
||||
# tell GUI the progress
|
||||
downloaded_bytes = fp.tell()
|
||||
percent = round((1.0 * downloaded_bytes / filesize) * 100, 2);
|
||||
percent = round((1.0 * downloaded_bytes / zip_filesize) * 100, 2);
|
||||
sys.stdout.write("\r{0}, {1}% ".format(helpers.human_readable_filesize(downloaded_bytes), percent))
|
||||
sys.stdout.flush()
|
||||
add_request(REQUEST_PROGRESS, path, { 'id':download_id, 'bytes':downloaded_bytes })
|
||||
@ -116,7 +141,7 @@ def download(slug_candidate):
|
||||
shutdown_func()
|
||||
|
||||
r = Response(generate())
|
||||
r.headers.add('Content-Length', filesize)
|
||||
r.headers.add('Content-Length', zip_filesize)
|
||||
r.headers.add('Content-Disposition', 'attachment', filename=basename)
|
||||
# guess content type
|
||||
(content_type, _) = mimetypes.guess_type(basename, strict=False)
|
||||
|
Loading…
Reference in New Issue
Block a user