#!/usr/bin/env python3 import argparse,json,sys,os from shutil import copyfile from tabulate import tabulate from datetime import datetime from utils 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 = [] header = ('Name', 'Path','Public','Password','Token','Upload','Overwrite','Direct','Expire','AutoRemove','Recipient','Description') short_header = ('Name', 'Path','Pub','Pwd','Up','Drct','Exp') 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) autoremove = get_or_none('autoremove', share, 0) if autoremove > 0: autoremove = "%d d"%( autoremove, ) else: autoremove = "-" direct = get_or_none('direct_links',share, False) if password else False expire = get_or_none('expire',share, "-") if not opts.verbose: expire = "N" if expire == "-" else "Y" description = get_or_none('description',share, "")[0:20] table.append(( share['name'], share['path']+"/", public, password, tokens, upload, overwrite, direct, expire, autoremove, get_or_none('recipient', share, "")[0:20], description )) table.sort(key = lambda x: x[0]) if not opts.verbose: short_table_indices = [0,1,2,3,5,7,8] header = short_header table = [[bool_short(row[col]) for col in short_table_indices] for row in table] print( tabulate( table, headers = header ) ) 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 = [] 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 )) table.sort(key = lambda x: x[0]) print( tabulate( table, headers = ('Path','Share','Size','Unit') ) ) def list_versions(shares, config, opts): 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 = [] header = ['Share', 'Path', 'File', 'Size', 'Unit'] if opts.delete == None: header.append('Age') else: header.append('Delete') now = datetime.now() for share in shares: if opts.name: if share['name'] != opts.name: continue version_folder = os.path.join( data_folder, share['path'], config['version_folder'] ) if not os.path.isdir(version_folder): table.append(( share['name'], share['path']+'/', '-', '-', '-', '-' )) else: for filename in sorted(os.listdir(version_folder)): full_path = os.path.join(version_folder, filename) if os.path.isdir(full_path): size = get_folder_size(full_path) else: size = os.path.getsize(full_path) (size_num, size_unit) = file_size_human( size, HTML=False ).split(" ",1) parsed_date = version_date(filename) if parsed_date == None: age_str = '-' else: age = now - parsed_date age_str = "%d d"%( age.days, ) if opts.delete != None: to_delete = age.days >= opts.delete age_str = "To Del" if to_delete else "Keep" if not opts.dry and to_delete: age_str = "Deleted" os.remove(full_path) table.append(( share['name'], share['path'] + '/', filename, size_num, size_unit, age_str )) table.sort(key = lambda x: x[0]) print(tabulate(table, headers = header)) def list_autoremove(shares, config, opts): 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 = [] header = ['Path', 'Size', 'Age', 'ToDelete'] for share in shares: if opts.name: if share['name'] != opts.name: continue share_folder = os.path.join( data_folder, share['path'], ) autoremove = get_or_none('autoremove', share, 0) if autoremove == 0: autoremove = False del header[-1] for filename in iter_folder_files(share_folder): full_path = os.path.join(share_folder, filename) if os.path.isdir(full_path): continue size = os.path.getsize(full_path) size_str = file_size_human( size, HTML = False ) age, age_str = file_age(full_path) to_delete = age.days >= autoremove if not autoremove: to_delete = False del_str = "Yes" if to_delete else "No" if opts.delete: if to_delete: del_str = "Deleted" os.remove(full_path) row_data = [ os.path.join( share['path'], filename ), size_str, age_str, del_str ] if not autoremove: del row_data[-1] table.append(row_data) table.sort(key = lambda x: x[0]) print(tabulate(table, headers = header)) 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, 'autoremove': opts.autoremove } 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 if opts.autoremove != None: share['autoremove'] = opts.autoremove # 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 not opts.expire == False: 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) elif opts.type == "flip": print_rest_api_flip(config, share, token) elif opts.type == "client": print_rest_api_client(config, share, token) def print_rest_api_client(config, share, token): print("Command to run python client:") print("python <( curl -s %s/script/client/%s/%s )"%( config['public_url'], share['name'], token )) 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_list(config, share): print("Link to enter the share:") print("%s/list/%s"%( config['public_url'], share['name'] )) 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, version_folder = config['version_folder']): 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, version_folder = config['version_folder']): 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_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# python2 <( curl -s %s/script/upload_split/%s/%s ) [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%( config['public_url'], share['name'], token )) def print_rest_api_flip(config, share, token): print("flip: fleese clipboard client download:") print("%s/script/flip/%s/%s"%( 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") parser_list.add_argument('--verbose', '-v', action="store_true", dest="verbose", default = False, help = "Verbose listing") ## list folders parser_folders = subparsers.add_parser('folders', help = "List the subfolders in data folder, and their disk usage") ## list versions parser_versions = subparsers.add_parser('versions', help = "List the old versions stored in shares, and their disk usage") parser_versions.add_argument('--delete', action="store", dest="delete", default = None, type = int, help = "Delete old versions, older than N days.") parser_versions.add_argument('--dry', action="store_true", dest="dry", default = False, help = "Do not actually delete files.") parser_versions.add_argument('-n','--name', action="store", dest="name", required = False, default = None, help = "Show / Delete only this share versions. If omitted, applies to all shares") ## list autoremove parser_autoremove = subparsers.add_parser( 'ages', help = "List the file ages in shares, and their disk usage" ) parser_autoremove.add_argument( '--delete', action = "store_true", dest = "delete", default = False, help = "Delete files older than share autoremove value." ) parser_autoremove.add_argument( '-n','--name', action = "store", dest = "name", required = True, default = None, help = "Share name to show / delete from.") ## 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( '--rm','--autoremove', action = "store", dest = "autoremove", default = 0, type = int, help = "Remove files older than N days. 0 disables the feature." ) 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( '--rm','--autoremove', action = "store", dest = "autoremove", default = None, type = int, help = "Remove files older than N days. 0 disables the feature." ) 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 = ['client','direct','download','flip','list','login','upload','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) if opts.subparser_name == 'versions': list_versions(shares,config,opts) if opts.subparser_name == 'ages': list_autoremove(shares,config,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()