support for multiple files and folders (#66)

This commit is contained in:
Micah Lee 2014-08-27 13:51:39 -07:00
parent e34a88b112
commit c5ced60f8b
5 changed files with 216 additions and 129 deletions

View File

@ -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()

View File

@ -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 }} &#x25BC;</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 }} &#x25BC;</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>

View File

@ -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")

View File

@ -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}.",

View File

@ -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)