Files
flees/code/templates/client.py
2021-10-18 11:31:12 +03:00

407 lines
11 KiB
Python

#!/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 | python3 -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)