From 376869f1f38a7e4920e5f7f22f616e73bbeb374d Mon Sep 17 00:00:00 2001 From: Ville Rantanen Date: Mon, 19 Mar 2018 10:25:40 +0200 Subject: [PATCH] somewhat functioning flees client --- code/app.py | 49 +++++-- code/flees-manager.py | 2 +- code/templates/client.py | 301 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 13 deletions(-) create mode 100644 code/templates/client.py diff --git a/code/app.py b/code/app.py index 01a3b15..df4867a 100644 --- a/code/app.py +++ b/code/app.py @@ -215,6 +215,30 @@ def send(name): return render_template('send.html',name=name) +@app.route('/file/list//', methods=['GET']) +def file_list(name, token): + (ok,share) = get_share(name, token = token) + if not ok: + return share + files = [] + for file in iter_folder_files(share['path']): + files.append(path2url(file)) + files.append("") + return "\n".join(files), 200 + + +@app.route('/file/details//', methods=['GET']) +def file_details(name, token): + (ok,share) = get_share(name, token = token) + if not ok: + return share + files = [] + for file in iter_folder_files(share['path']): + status = file_stat(share['path'],file) + files.append(status) + return jsonify(files), 200 + + @app.route('/file/size///', methods=['GET']) def file_size(name, token, filename): (ok,share) = get_share(name, token = token) @@ -230,18 +254,6 @@ def file_size(name, token, filename): return str(size), 200 -@app.route('/file/list//', methods=['GET']) -def list_files(name, token): - (ok,share) = get_share(name, token = token) - if not ok: - return share - files = [] - for file in iter_folder_files(share['path']): - files.append(path2url(file)) - files.append("") - return "\n".join(files), 200 - - @app.route('/list//', methods=['GET']) @app.route('/list/', methods=['GET']) def list_view(name, token = None): @@ -350,6 +362,19 @@ def download_zip(name, token = None): ) +@app.route('/script/client//', methods=['GET']) +def script_client(name = None, token = None): + (ok,share) = get_share(name, token = token) + if not ok: + return share + return render_template( + "client.py", + name = name, + token = token, + rooturl = request.url_root + ) + + @app.route('/script/upload//', methods=['GET']) def script_upload(name = None, token = None): (ok,share) = get_share(name, token = token) diff --git a/code/flees-manager.py b/code/flees-manager.py index 6124b26..ad0dbdd 100755 --- a/code/flees-manager.py +++ b/code/flees-manager.py @@ -402,7 +402,7 @@ def print_rest_api_upload(config, share, token): share['name'], token )) - print("\nLink to upload multiple files to the share, splitting large files (default 512Mb):") + print("\nLink to upload multiple files to the share, splitting large files:") print("\n# curl -s %s/script/upload_split/%s/%s | python - [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%( config['public_url'], share['name'], diff --git a/code/templates/client.py b/code/templates/client.py new file mode 100644 index 0000000..9ec0ae2 --- /dev/null +++ b/code/templates/client.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +import argparse, sys, os, subprocess, time, re, json +from subprocess import call, Popen, PIPE, STDOUT +import threading +import readline +from tabulate import tabulate +import glob +from io import StringIO + +ROOTURL="{{ rooturl }}" +SHARE="{{ name }}" +TOKEN="{{ token }}" + + +class Completer(object): + + def __init__(self, choices = []): + self.choices = choices + self.space = re.compile('.*\s+$', re.M) + + def _listdir(self, path): + "List directory 'root' appending the path separator to subdirs." + files = [] + for name in os.listdir(path): + file_path = os.path.join(path, name) + if os.path.isdir(file_path): + name += os.sep + files.append(name) + return files + + def _complete_path(self, path=None): + "Perform completion of filesystem path." + if not path: + return self._listdir('.') + path = os.path.expanduser(path) + dirname, rest = os.path.split(path) + tmp = dirname if dirname else '.' + res = [os.path.join(dirname, p) + for p in self._listdir(tmp) if p.startswith(rest)] + # more than one match, or single match which does not exist (typo) + if len(res) > 1 or not os.path.exists(path): + return res + # resolved to a single directory, so return list of files below it + if os.path.isdir(path): + return [os.path.join(path, p) for p in self._listdir(path)] + # exact file match terminates this completion + return [path + ' '] + + + def complete(self, text, state): + "Generic readline completion entry point." + buffer = readline.get_line_buffer() + if len(self.choices) == 0: + return (self._complete_path(buffer) + [None])[state] + + # show all commands + if len(buffer) == 0: + return [c + ' ' for c in self.choices][state] + # resolve command to the implementation function + cmd = buffer.strip() + results = [c + ' ' for c in self.choices if c.startswith(cmd)] + [None] + return results[state] + + +def download_file(file, opts): + print("Download "+file['name']) + cmd = [ + 'curl','--create-dirs', + '-o', file['name'], + '%s%s/%s/%s/%s'%( + opts.rooturl, + "download", + opts.share, + opts.token, + file['url'] + ), + ] + p = Popen( + cmd, + stderr = PIPE, + ) + for char in iter(lambda: p.stderr.read(1), ''): + if not char: + return + sys.stderr.write(char) + sys.stderr.flush() + return + + +def menu(opts): + commands = [ + 'Download', + 'Upload' + ] + print_title("Main") + for i,command in enumerate(commands): + print(" %d. %s"%( i+1, command, )) + comp = Completer(choices = ["1","2"]) + readline.set_completer(comp.complete) + choice = user_input("Number of action: ").strip() + if choice == "1": + menu_download(opts) + if choice == "2": + menu_upload(opts) + if choice == "": + sys.exit(0) + + +def menu_download(opts): + while True: + print_title("Download files") + files = json.loads(run_command("file/details", opts)) + file_table = [] + for f in files: + file_table.append((f['name'], float(f['size'].replace(",","")), f['mtime'])) + print(tabulate(file_table, headers = ("Name", "Size [Mb]", "Modified"))) + name_list = [x['name'] for x in files] + comp = Completer(choices = name_list) + # we want to treat '/' as part of a word, so override the delimiters + readline.set_completer(comp.complete) + print("\n[Empty to return, * globs]") + choice = user_input('Download file: ').strip() + if choice == "": + return + if "*" in choice: + choice = choice.replace('*','.*') + choice_re = re.compile(choice) + choices = [i for i,x in enumerate(name_list) if choice_re.match(x)] + else: + if choice not in name_list: + print("No such file") + continue + choices = [ name_list.index(choice) ] + for choice in choices: + download_file(files[choice], opts) + + +def menu_upload(opts): + while True: + print_title("Upload") + comp = Completer(choices = []) + # we want to treat '/' as part of a word, so override the delimiters + + readline.set_completer(comp.complete) + print("\n[Empty to return, * globs]") + choice = user_input('Upload file: ').strip() + if choice == "": + return + if "*" in choice: + choices = glob.glob(choice) + else: + if not os.path.exists(choice): + print("No such file") + continue + choices = [ choice ] + for choice in choices: + upload_file(choice, opts) + + +def parse_options(): + parser = argparse.ArgumentParser(description='Flees client') + parser.add_argument('-s', action="store", type=int, dest="split", default = 32, + help = "Split size in megabytes [%(default)s]" + ) + parser.add_argument('--rooturl', action="store", dest="rooturl", + default = ROOTURL, + help = "Address of Flees server [%(default)s]" + ) + parser.add_argument('--share', action="store", dest="share", + default = SHARE, + help = "Name of Flees share [%(default)s]" + ) + parser.add_argument('--token', action="store", dest="token", + default = TOKEN, + help = "API token for the share [%(default)s]" + ) + return parser.parse_args() + + +def print_title(title): + print("%s\n%s\n"%( + title, + "="*len(title) + )) + + +def read_output(stream): + for char in iter(lambda: stream.read(1), ''): + if not char: + break + sys.stderr.write(char) + sys.stderr.flush() + stream.close() + + +def run_command(command, opts): + p = Popen( + [ + 'curl','-s', + '%s%s/%s/%s'%(opts.rooturl, command, opts.share, opts.token) + ], + stdout=PIPE, + stderr=PIPE + ) + stdout_data, stderr_data = p.communicate() + return stdout_data + + +def run_loop(opts): + while True: + menu(opts) + + +def upload_file(file, opts): + print("Upload "+file) + p = Popen( + "curl -s %s%s/%s/%s | python -u - -s %d %s"%( + opts.rooturl, + "script/upload_split", + opts.share, + opts.token, + opts.split, + file + ), + stdout = PIPE, + shell = True + ) + read_output(p.stdout) + return + + + cmd1 = [ + 'curl','-s', + '%s%s/%s/%s'%( + opts.rooturl, + "script/upload_split", + opts.share, + opts.token, + ), + ] + cmd2 = [ + 'python', '-', + '-s', str(opts.split), + file + ] + p1 = Popen( + cmd1, + stderr = PIPE, + stdout = PIPE, + ) + script, stderr = p1.communicate() + p2 = Popen( + cmd2, + stdin = PIPE, + stdout = PIPE, + stderr = PIPE, + bufsize = 1 + ) + p2.stdin.write(script) + p2.stdin.close() + for char in iter(lambda: p2.stderr.read(1), ''): + if not char: + break + sys.stderr.write(char) + sys.stderr.flush() + + #~ read_output(p2.stdout) + #~ tin = threading.Thread( + #~ target=write_input, args=(p2.stdin, script) + #~ ) + #~ tout = threading.Thread( + #~ target=read_output, args=(p2.stdout,) + #~ ) + #~ terr = threading.Thread( + #~ target=read_output, args=(p2.stderr,) + #~ ) + #~ for t in (tin, tout, terr): + #~ t.daemon = True + #~ t.start() + #~ p2.wait() + return + + +def user_input(arg): + try: + return raw_input(arg) + except NameError: + return input(arg) + + +def write_input(stream, string): + stream.write(string) + stream.close() + + +if __name__ == "__main__": + readline.set_completer_delims(' \t\n;') + readline.parse_and_bind("tab: complete") + opts = parse_options() + run_loop(opts) +