From c5ced60f8bafbbc0a5eff687927f8b2ddb0341ad Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 27 Aug 2014 13:51:39 -0700 Subject: [PATCH] support for multiple files and folders (#66) --- onionshare/helpers.py | 62 ++++++++++---- onionshare/index.html | 171 ++++++++++++++++++++++----------------- onionshare/onionshare.py | 49 +++++------ onionshare/strings.json | 6 +- onionshare/web.py | 57 +++++++++---- 5 files changed, 216 insertions(+), 129 deletions(-) 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 }}

+ + + + + + + {% for info in file_info.dirs %} + + + + + + {% endfor %} + {% for info in file_info.files %} + + + + + + {% endfor %} +
{{strings.filename}}{{strings.size}}
{{ info.basename }}{{ info.size_human }}
{{ info.basename }}{{ info.size_human }}
+ 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)