diff --git a/onionshare/helpers.py b/onionshare/helpers.py
index 83e04d78..9d9193bc 100644
--- a/onionshare/helpers.py
+++ b/onionshare/helpers.py
@@ -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()
diff --git a/onionshare/index.html b/onionshare/index.html
index e6d423ba..3d5cde23 100644
--- a/onionshare/index.html
+++ b/onionshare/index.html
@@ -1,76 +1,103 @@
-
- OnionShare
-
-
-
-
-
-
- {{ filename }} ▼
+
+ OnionShare
+
+
+
+
+
+ {{ filename }} ▼
+ {{strings.download_size}}: {{ filesize_human }}
+
+
+ |
+ {{strings.filename}} |
+ {{strings.size}} |
+
+ {% for info in file_info.dirs %}
+
+ ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQXOpwjbQAAAM5JREFUSMftlksOwyAMRDFCbNjkDL1kz9K75Cq9AZF3kYIM7oqoP/KpcFK1mRUrnsbW2Fbq0N+JiPhZRMS1OZAfzLzocwCAGmC9V2Vhjduarj8CzynGqIwxsDl4SXV267E4uFTN33X8deCxDyV1XXeRYJo1UUDEa9M0pxoRK+b4HiqRcz3nVAJKRK+TSxqaGbrkNKUkBn0oNSK2+T0MA1dau9NzO4QwuvPen1lAxQtEsq/vNpSWhvZ9v/3IRMTWOQezkyuEoKy14tfHoU11A6Mr5AxrpuMVAAAAAElFTkSuQmCC) |
+ {{ info.basename }} |
+ {{ info.size_human }} |
+
+ {% endfor %}
+ {% for info in file_info.files %}
+
+ ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQqYvRvfAAAAJFJREFUSMftlUkOwCAIAEWJ//+uIUivtYmtG7VN8KYHRhlA5zYtuB6klCTG2BcEAHrBeN4ws3jvX3lxQZmFiogMgVesVrhKXlvgakKf4KqVdAdXL+Ea3O8aILh0GnUMEhjtw9mLbUv1tx33fgItynBVoN+k2hybY3Nsjg08Bs45q4GYuQ4OIQARLYcSkUPEoiUP4j5IFasKOnUAAAAASUVORK5CYII=) |
+ {{ info.basename }} |
+ {{ info.size_human }} |
+
+ {% endfor %}
+
+
diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py
index ca000c7a..69b4e505 100644
--- a/onionshare/onionshare.py
+++ b/onionshare/onionshare.py
@@ -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")
diff --git a/onionshare/strings.json b/onionshare/strings.json
index f98a366d..312e86bd 100644
--- a/onionshare/strings.json
+++ b/onionshare/strings.json
@@ -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}.",
diff --git a/onionshare/web.py b/onionshare/web.py
index 9f16cdcb..c9e2a6a4 100644
--- a/onionshare/web.py
+++ b/onionshare/web.py
@@ -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)