#!/usr/bin/env python3 import argparse, sys, os, subprocess, time, re, json from subprocess import call, Popen, PIPE, STDOUT import readline import glob from io import StringIO import tempfile, hashlib try: from tabulate import tabulate except ImportError: print("Install tabulate with: pip install --user tabulate") def tabulate(table, headers): value = [] value.append("\t".join(headers)) for row in table: value.append("\t".join([str(c) for c in row])) value.append("\n\nGet nicer tables by installing 'tabulate' package!\n") return "\n".join(value) 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 check_connection(opts): p = Popen( [ 'curl','-s','-f', '%s%s/%s/%s'%(opts.rooturl, "file/list", opts.share, opts.token) ], stdout=PIPE, stderr=PIPE ) p.communicate() rc = p.returncode if rc != 0: return False return True def download_file(file, opts, filename = False): print("Download " + file['name']) if not filename: filename = file['name'] cmd = [ 'curl','--create-dirs', '-o', filename, '%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.decode()) sys.stderr.flush() return def file_hash(file): buf = 65536 # lets read stuff in 64kb chunks! md5 = hashlib.md5() with open(file, 'rb') as f: while True: data = f.read(buf) if not data: break md5.update(data) return md5.hexdigest() def edit_file(file): old_hash = file_hash(file) editor = os.environ.get('EDITOR','vim') call([editor, file]) new_hash = file_hash(file) return new_hash != old_hash def menu(opts): print_title("Main menu") commands = { '4': {'name': 'Change login', 'cmd': menu_login} } if check_connection(opts): commands['1'] = {'name': 'Download', 'cmd': menu_download} commands['2'] = {'name': 'Upload', 'cmd': menu_upload} commands['3'] = {'name': 'Edit', 'cmd': menu_edit} else: print(" Not a valid login") for command in sorted(commands): print(" %s. %s"%( command, commands[command]['name'])) comp = Completer(choices = commands.keys()) readline.set_completer(comp.complete) print("\n[Empty to exit]") choice = user_input("Number of action: ").strip() if choice in commands: value = commands[choice]['cmd'](opts) if isinstance(value, dict): opts = value else: sys.exit(0) return opts 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_edit(opts): while True: print_title("Edit file") files = json.loads(run_command("file/details", opts)) file_table = [] for f in files: if not f['editable']: continue 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, new name to create new file]") choice = user_input('Edit file: ').strip() if choice == "": return tfp, tmp = tempfile.mkstemp( suffix = ".txt" ) if choice in name_list: file = [f for f in files if f['name'] == choice][0] download_file(file, opts, filename = tmp) changed = edit_file(tmp) if changed: upload_named_file(tmp, opts, choice) os.remove(tmp) def menu_login(opts): print_title("Login") print("\n[Empty to keep previous value]") choice = user_input('Host [%s]: '%( opts.rooturl, )).strip() if choice != "": opts.rooturl = choice choice = user_input('Share name [%s]: '%( opts.share, )).strip() if choice != "": opts.share = choice choice = user_input('Token [%s]: '%( opts.token, )).strip() if choice != "": opts.token = choice return opts def menu_upload(opts): while True: print_title("Upload") files = sorted(os.listdir(".")) file_table = [] for f in files: if f.startswith("."): continue if os.path.isfile(f): file_size = round(float(os.path.getsize(f)) / 1024 / 1024, 2) else: file_size = "[DIR]" f += "/" file_table.append((f, file_size)) print(tabulate(file_table, headers = ("Name", "Size [Mb]"))) 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): status = "Share: %s, Server: %s"%( opts.share, opts.rooturl ) pad = " "*(int((len(status) - len(title))/2)) print("\n".join(( "", "="*len(status), status, pad + title + pad, "="*len(status) ))) def read_output(stream): for char in iter(lambda: stream.read(1), ''): if not char: break sys.stderr.write(char.decode()) 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): try: while True: opts = menu(opts) except KeyboardInterrupt: print("") return 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 def upload_named_file(file, opts, filename): print("Upload " + filename) p = Popen( "curl -F 'file=@%s;filename=%s' %s%s/%s/%s"%( file, filename, opts.rooturl, "upload", opts.share, opts.token ), stdout = PIPE, shell = True ) read_output(p.stdout) 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)