Files
flees/code/flees-manager.py
2018-04-16 15:30:58 +03:00

665 lines
23 KiB
Python
Executable File

#!/usr/bin/env python
import argparse,json,sys,os
from shutil import copyfile
from tabulate import tabulate
from datetime import datetime
from utils.utils import *
from utils.crypt import *
def get_root_path(opts):
root_folder = os.path.dirname(
os.path.dirname(
os.path.abspath(
opts.config
)
)
)
return root_folder
def list_shares(shares,opts):
table = []
table.append(('Name', 'Path','Public','Password','APIToken','Upload','Overwrite','Direct','Expire','Recipient','Description'))
for share in shares:
public = get_or_none('public',share, False)
password = 'pass_hash' in share
tokens = 'tokens' in share
if tokens:
tokens = len(share['tokens']) > 0
upload = get_or_none('upload',share, False)
overwrite = get_or_none('overwrite',share, True)
direct = get_or_none('direct_links',share, False) if password else False
expire = get_or_none('expire',share, "-")
description = get_or_none('description',share, "")[0:20]
table.append((
share['name'],
share['path']+"/",
public,
password,
tokens,
upload,
overwrite,
direct,
expire,
get_or_none('recipient', share, "")[0:20],
description
))
print(tabulate(table, headers = "firstrow"))
def list_folders(shares,config):
if 'data_folder' not in config:
print("data_folder not defined in config")
sys.exit(1)
data_folder = os.path.join(config['__root_path__'], config['data_folder'])
table = []
table.append( ('Path','Share','Size','Unit') )
for path, folders, files in os.walk(data_folder):
full_path = os.path.join(data_folder, path)
share_name = None
parent_is_share = False
for share in shares:
share_path = os.path.join(data_folder, share['path'])
if not os.path.exists(share_path):
break
if os.path.samefile(full_path, share_path):
share_name = share['name']
break
parents = full_path.split(os.sep)
for p in range(len(parents)):
test_path = os.sep+os.sep.join(parents[1:(p+2)])
if os.path.samefile(test_path, share_path):
parent_is_share = True
if parent_is_share:
continue
if share_name == None:
# skip folder if it's not a share, and not a leaf
if len(folders) > 0:
continue
share_name = "[Unused]"
(size_num, size_unit) = file_size_human(
get_folder_size(
full_path
),
HTML=False
).split(" ",1)
table.append((
path,
share_name,
size_num,
size_unit
))
print(tabulate(table, headers = "firstrow"))
def add_share(shares, config, opts):
# Make name and path safe:
opts.name = safe_name(opts.name)
opts.path = safe_path(opts.path)
# check for share name exists already
for share in shares:
if share['name'] == opts.name:
print("Share with name '%s' already exists"%( opts.name, ))
sys.exit(1)
share = {
'name': opts.name,
'path': opts.path,
'public': opts.public,
'upload': opts.upload,
'overwrite': opts.overwrite,
'direct_links': opts.direct,
'description': opts.description,
'recipient': opts.recipient
}
if opts.password:
if opts.plain:
share['pass_plain'] = opts.password
share['pass_hash'] = password_hash(opts.password, config['app_secret_key'])
share['tokens'] = [random_token()]
if opts.expire:
try:
date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M")
except ValueError as e:
print(e)
print("Date format error")
sys.exit(1)
share.update({
'expire': opts.expire
})
print_share(share, config)
if opts.write:
shares.append(share)
shares_file = os.path.join(config['__root_path__'], opts.shares_file)
if os.path.exists(shares_file):
print("creating backup %s"%(shares_file+".bkp",))
copyfile(
shares_file,
shares_file+".bkp"
)
with open(shares_file,'wt') as fp:
json.dump(shares, fp, indent = 2, sort_keys = True)
print("Wrote file %s"%(shares_file,))
print("Add share: %s"%( opts.name, ))
check_login(share, config)
else:
print("Share not saved anywhere.")
def check_login(share, config):
import requests
print("Login link")
URL = None
if 'tokens' in share:
if len(share['tokens'])>0:
token = share['tokens'][0]
URL = "%s/list/%s/%s"%(
config['public_url'],
share['name'],
token
)
if URL == None:
URL = "%s/list/%s"%(
config['public_url'],
share['name']
)
print(URL)
req = requests.get(URL)
if (req.status_code != 200):
print("Login did not appear to work")
def modify_share(shares, config, opts):
print("Modifying share: %s"%( opts.name, ))
found = False
for i,share in enumerate(shares):
if share['name'] != opts.name:
continue
orig_share = dict(share)
if 'tokens' in share:
orig_share['tokens'] = list(share['tokens'])
print_share(share, config)
found = True
break
if not found:
print('no such share')
sys.exit(1)
if opts.path != None:
share['path'] = safe_path(opts.path)
for attr in ('public','upload','direct_links','overwrite'):
if getattr(opts,attr) != None:
share[attr] = getattr(opts,attr) == 'true'
if opts.description != None:
share['description'] = opts.description
if opts.recipient != None:
share['recipient'] = opts.recipient
# REMOVE password
if opts.password == "":
if 'pass_plain' in share:
del share['pass_plain']
if 'pass_hash' in share:
del share['pass_hash']
if opts.password:
# ADD/Change a password
if opts.plain:
share['pass_plain'] = opts.password
share['pass_hash'] = password_hash(opts.password, config['app_secret_key'])
# Handle tokens
if opts.remove_tokens:
for token in opts.remove_tokens:
if token in share['tokens']:
share['tokens'].remove(token)
if opts.tokens:
for token in opts.tokens:
if not 'tokens' in share:
share['tokens'] = []
if not token in share['tokens']:
share['tokens'].append(token)
if opts.expire:
if opts.expire == "":
# REMOVE EXPIRATION
if 'expire' in share:
del share['expire']
else:
# ADD/CHANGE EXPIRATION
try:
date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M")
except ValueError as e:
print(e)
print("Date format error")
sys.exit(1)
share['expire'] = opts.expire
if opts.write:
shares[i] = share
shares_file = os.path.join(config['__root_path__'], opts.shares_file)
if os.path.exists(shares_file):
print("creating backup %s"%(shares_file+".bkp",))
copyfile(
shares_file,
shares_file+".bkp"
)
with open(shares_file,'wt') as fp:
json.dump(shares, fp, indent = 2, sort_keys = True)
print("Wrote file %s"%(shares_file,))
modified = []
for key in share:
if not key in orig_share:
modified.append(key)
continue
if str(orig_share[key]) != str(share[key]):
modified.append(key)
continue
for key in orig_share:
if not key in share:
modified.append(key)
print("Modified values: %s"%(", ".join(modified)))
print_share(share, config)
if not opts.write:
print("Share not saved anywhere.")
def remove_share(shares,config,opts):
name = opts.name
share = [share for share in shares if share['name'] == name]
for share_ in share:
print("Removing share: %s"%( name, ))
print(json.dumps(share_, indent = 2, sort_keys = True))
if len(share) == 0:
print("No such share")
sys.exit(1)
if opts.write:
shares = [share for share in shares if share['name'] != name]
shares_file = os.path.join(config['__root_path__'], opts.shares_file)
print("creating backup %s"%(shares_file+".bkp",))
copyfile(shares_file, shares_file+".bkp")
with open(shares_file,'wt') as fp:
json.dump(shares, fp, indent = 2, sort_keys = True)
print("Removed %s from %s"%(name, shares_file))
else:
print("Share was not actually removed. Use -w to rewrite shares file.")
def show_share(shares, config, opts):
found = False
for share in shares:
if share['name'] != opts.name:
continue
found = True
break
if not found:
print('no such share')
sys.exit(1)
if not opts.show_password:
if 'pass_plain' in share:
share['pass_plain'] = "--HIDDEN--"
if 'pass_hash' in share:
share['pass_hash'] = "--HIDDEN--"
if 'tokens' in share:
share['tokens'] = ["--HIDDEN--"]
if not 'expire' in share:
share['expire'] = "--NEVER--"
print_share(share, config)
def print_rest_api(shares, config, opts):
if 'public_url' not in config:
print("Set public_url variable in your config.json")
sys.exit(1)
shares = [share for share in shares if share['name'] == opts.name]
if len(shares) == 0:
print("No such share %s"%( opts.name, ))
sys.exit(1)
share = shares[0]
if opts.type == "list":
print_rest_api_list(config,share)
return
if (not 'tokens' in share) or len(share['tokens']) == 0:
print("REST API enabled only if tokens are set for the share")
sys.exit(1)
token = False
if len(share['tokens']) == 1:
token = share['tokens'][0]
else:
try:
token_int = int(opts.token) - 1
if token_int < 0:
raise ValueError
token = share['tokens'][token_int]
except (IndexError, ValueError, TypeError) as e:
if opts.token in share['tokens']:
token = opts.token
if not token:
# more tokens!
if opts.token:
print("No such token for this share")
print("Tokens:")
for i,token in enumerate(share['tokens']):
print("%d. %s"%( i+1, token ))
print("Run again with --token [nr]")
if not opts.token:
sys.exit(0)
else:
sys.exit(1)
if opts.type == "login":
print_rest_api_login(config,share,token)
elif opts.type == "upload":
print_rest_api_upload(config,share,token)
elif opts.type == "download":
print_rest_api_download(config, share, token, opts.filename)
elif opts.type == "direct":
print_rest_api_direct(config, share, token, opts.filename)
elif opts.type == "zip":
print_rest_api_zip(config, share, token)
def print_rest_api_list(config, share):
print("Link to enter the share:")
print("%s/list/%s"%(
config['public_url'],
share['name']
))
def print_rest_api_login(config, share, token):
print("Link to automatically login in the share:")
print("%s/list/%s/%s"%(
config['public_url'],
share['name'],
token
))
def print_rest_api_upload(config, share, token):
if 'upload' not in share or not share['upload']:
print("Uploading not allowed to this share")
sys.exit(0)
print("Link to upload file to the share:")
print("\n# curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%(
config['public_url'],
share['name'],
token
))
print("\nLink to upload multiple files to the share:")
print("\n# curl -s %s/script/upload/%s/%s | bash /dev/stdin file_to_upload.ext [second.file.ext]"%(
config['public_url'],
share['name'],
token
))
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'],
token
))
def print_rest_api_download(config, share, token, show_filename):
if not show_filename:
print("Links to download files:")
share_path = os.path.join(
config['__root_path__'],
config['data_folder'],
share['path']
)
if not os.path.exists(share_path):
print("no files")
sys.exit(0)
for filename in iter_folder_files(share_path):
if show_filename:
if filename != show_filename:
continue
print("%s/download/%s/%s/%s"%(
config['public_url'],
share['name'],
token,
path2url(filename)
))
if not show_filename:
print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%(
config['public_url'],
share['name'],
token
))
def print_rest_api_direct(config, share, token, show_filename):
if 'direct_links' not in share or not share['direct_links']:
print("Direct downloading not allowed in this share")
sys.exit(0)
if not show_filename:
print("Links to direct download files:")
share_path = os.path.join(
config['__root_path__'],
config['data_folder'],
share['path']
)
if not os.path.exists(share_path):
print("no files")
sys.exit(0)
for filename in iter_folder_files(share_path):
if show_filename:
if filename != show_filename:
continue
print("%s/direct/%s/%s/%s"%(
config['public_url'],
share['name'],
get_direct_token(share,filename),
path2url(filename)
))
if not show_filename:
print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%(
config['public_url'],
share['name'],
token
))
def print_rest_api_zip(config, share, token):
print("ZIP download:")
print("%s/zip/%s/%s"%(
config['public_url'],
share['name'],
token
))
def print_share(share, config):
share = dict(share)
if 'tokens' in share:
share['tokens'] = list(share['tokens'])
data_folder = os.path.join(config['__root_path__'], config['data_folder'])
share_path = os.path.join(data_folder, share['path'])
share['path'] = share_path
if 'tokens' in share:
for i,token in enumerate(share['tokens']):
share['token%d'%(i+1,)] = token
del share['tokens']
table = [('name',share['name'])]
for key in sorted(share):
if key != 'name':
table.append((key,share[key]))
print(tabulate(table))
#~ print(json.dumps(share, indent = 2, sort_keys = True))
def print_token():
print(random_token())
def parse_options():
config_default = os.path.realpath(
os.path.join(
os.path.dirname(
os.path.realpath(__file__)
),
"..",
"data",
"config.json"
)
)
parser = argparse.ArgumentParser(description='Flees share manager')
parser.add_argument('-c','--config', action="store", dest="config", default = config_default,
help = "Your current config.json file [%(default)s]")
parser.add_argument('-s','--shares', action="store", dest="shares_file", default = None,
help = "shares.json you want to use. Defaults to what config.json defines")
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')
## list shares
parser_list = subparsers.add_parser('list', help = "List shares")
## list folders
parser_folders = subparsers.add_parser('folders', help = "List the subfolders in data folder, and their disk usage")
## Show
parser_show = subparsers.add_parser('show', help = "Show share")
parser_show.add_argument('-P', action="store_true", dest="show_password", default = False,
help = "Display passwords")
parser_show.add_argument(dest="name")
## Remove
parser_remove = subparsers.add_parser('remove', help = "Remove a share")
parser_remove.add_argument(dest="name")
parser_remove.add_argument('-w','--write', action="store_true", dest="write", default = False,
help = "Write changes to the shares.json file"
)
## Add
parser_add = subparsers.add_parser('add', help = "Add a share")
parser_add.add_argument('-n','--name', action="store", dest="name", required = True)
parser_add.add_argument('-p','--path', action="store", dest="path", required = True,
help= "path relative to data folder"
)
parser_add.add_argument('-D','--description', action="store", dest="description", default = "",
help= "Describe the contents"
)
parser_add.add_argument('-P','--public', action="store_true", dest="public", default = False)
parser_add.add_argument('-u','--upload', action="store_true", dest="upload", default = False)
parser_add.add_argument('-o','--overwrite', action="store_false", dest="overwrite", default = True,
help = "Disable file overwrites. If disabled, old files are versioned with modification date.")
parser_add.add_argument('-d','--direct', action="store_true", dest="direct", default = False,
help = "Allow direct file sharing (password hash included in URL)")
parser_add.add_argument('--pass-plain', action="store_true", dest="plain", default = False,
help = "Save the password as plain text")
parser_add.add_argument('--password', action="store", dest="password", default = False,
help = "Setting a password enables use of login links and direct downloads")
parser_add.add_argument('-e','--expire', action="store", dest="expire", default = False,
help = "expire date in format '%%Y-%%m-%%d %%H:%%M' ex. '2018-12-24 21:00'"
)
parser_add.add_argument('-r','--recipient', action="store", dest="recipient", default = "",
help= "Recipient for notifications (if enabled)"
)
parser_add.add_argument('--dry', action="store_false", dest="write", default = True,
help = "Do not write changes to the shares.json file"
)
## Modify
parser_modify = subparsers.add_parser('modify', help = "Modify share")
parser_modify.add_argument('-n','--name', action="store", dest="name", required = True)
parser_modify.add_argument('-p','--path', action="store", dest="path", default = None,
help= "path relative to data folder"
)
parser_modify.add_argument('-D','--description', action="store", dest="description", default = None,
help= "Describe the contents"
)
parser_modify.add_argument('-P','--public', action="store", dest="public", default = None, choices = ['true','false'])
parser_modify.add_argument('-u','--upload', action="store", dest="upload", default = None, choices = ['true','false'])
parser_modify.add_argument('-o','--overwrite', action="store", dest="overwrite", default = None, choices = ['true','false'],
help = "Disable file overwrites. If disabled, old files are versioned with modification date.")
parser_modify.add_argument('-d','--direct', action="store", dest="direct_links", default = None, choices = ['true','false'],
help = "Allow direct file sharing (password hash included in URL)")
parser_modify.add_argument('--pass-plain', action="store_true", dest="plain", default = False,
help = "Save the password as plain text")
parser_modify.add_argument('--password', action="store", dest="password", default = False,
help = "Setting a password enables use of login links and direct downloads. Set as empty string to remove password protection.")
parser_modify.add_argument('-e','--expire', action="store", dest="expire", default = False,
help = "expire date in format '%%Y-%%m-%%d %%H:%%M' ex. '2018-12-24 21:00'. Set as empty string to remove expiration."
)
parser_modify.add_argument('-r','--recipient', action="store", dest="recipient", default = None,
help= "Recipient for notifications (if enabled)"
)
parser_modify.add_argument('-t','--add-token', action="append", dest="tokens", default = [],
help= "Token for REST api, may be issued multiple times"
)
parser_modify.add_argument('--remove-token', action="append", dest="remove_tokens", default = [],
help= "Remove REST tokens, may be issued multiple times"
)
parser_modify.add_argument('--dry', action="store_false", dest="write", default = True,
help = "Do not write changes to the shares.json file"
)
## REST
parser_rest = subparsers.add_parser('rest', help = "Display REST API links")
parser_rest.add_argument(dest="name", help = "Name of the share")
parser_rest.add_argument(dest="type", help = "Type of command",
choices = ['list','login','upload','download','direct','zip']
)
parser_rest.add_argument(dest="filename", help = "File name for download/direct queries",
nargs = '?',
default = None
)
parser_rest.add_argument('-t','--token', action="store", dest="token", default = None,
help= "If share has multiple tokens, select one to print REST API for."
)
## TOKEN
parser_token = subparsers.add_parser('token', help = "Generate a random token")
return parser.parse_args()
if __name__ == "__main__":
opts = parse_options()
config = {}
if os.path.exists(opts.config):
config = json.load(open(opts.config,'rt'))
config['__root_path__'] = get_root_path(opts)
else:
print("config file %s does not exist!"%(opts.config,))
sys.exit(1)
if opts.shares_file:
config['shares_file'] = opts.shares_file
if 'shares_file' in config:
# if not from command line, read from config
opts.shares_file = config['shares_file']
if os.path.exists(os.path.join(config['__root_path__'],config['shares_file'])):
shares = json.load(open(os.path.join(config['__root_path__'],config['shares_file']),'rt'))
else:
print("shares_file %s does not exist!"%(os.path.join(config['__root_path__'],config['shares_file'])))
shares = []
if opts.subparser_name == 'list':
list_shares(shares,opts)
elif opts.subparser_name == 'folders':
list_folders(shares,config)
elif opts.subparser_name == 'show':
show_share(shares,config,opts)
elif opts.subparser_name == 'remove':
remove_share(shares,config,opts)
elif opts.subparser_name == 'add':
add_share(shares,config,opts)
elif opts.subparser_name == 'modify':
modify_share(shares,config,opts)
elif opts.subparser_name == 'rest':
print_rest_api(shares,config,opts)
elif opts.subparser_name == 'token':
print_token()