#!/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) elif opts.type == "direct": print_rest_api_direct(config, share, token) 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 (default 512Mb):") 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): 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): print("%s/download/%s/%s/%s"%( config['public_url'], share['name'], token, path2url(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): if 'direct_links' not in share or not share['direct_links']: print("Direct downloading not allowed in this share") sys.exit(0) 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): print("%s/direct/%s/%s/%s"%( config['public_url'], share['name'], get_direct_token(share,filename), path2url(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('-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()