363 lines
10 KiB
Python
363 lines
10 KiB
Python
#!/usr/bin/env python
|
|
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 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)
|
|
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):
|
|
commands = {
|
|
'1': {'name': 'Download', 'cmd': menu_download},
|
|
'2': {'name': 'Upload', 'cmd': menu_upload},
|
|
'3': {'name': 'Edit', 'cmd': menu_edit},
|
|
}
|
|
print_title("Main menu")
|
|
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:
|
|
commands[choice]['cmd'](opts)
|
|
else:
|
|
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_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_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 = " "*((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)
|
|
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:
|
|
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)
|