#!/usr/bin/env python import argparse,json,sys,os from shutil import copyfile from tabulate import tabulate import hashlib def get_folder_size(path): total_size = 0 for dirpath, dirnames, filenames in os.walk(path): for f in filenames: fp = os.path.join(dirpath, f) total_size += os.path.getsize(fp) return total_size def get_or_no(key,d,no): if key in d: return d[key] return no def get_direct_token(share, filename): if not 'pass_hash' in share: return None return hashlib.sha1( share['pass_hash'].encode('utf-8') + filename.encode('utf-8') ).hexdigest() def get_root_path(opts): root_folder = os.path.dirname( os.path.dirname( os.path.abspath( opts.config ) ) ) return root_folder def file_size_human(num): for x in ['B','KB','MB','GB','TB']: if num < 1024.0: if x=='B': return "%d %s" % (num, x) return "%3.1f %s" % (num, x) num /= 1024.0 def list_shares(shares,opts): table = [] table.append(('Name', 'Path','Public','Password','Upload','Overwrite','Direct','Expire')) for share in shares: public = get_or_no('public',share, False) password = 'pass_hash' in share or 'pass_plain' in share if opts.show_password: if not password: password = "" if 'pass_plain' in share: password = hashlib.sha1(share['pass_plain'].encode('utf-8')).hexdigest() if 'pass_hash' in share: password = share['pass_hash'] upload = get_or_no('upload',share, False) overwrite = get_or_no('overwrite',share, True) direct = get_or_no('direct_links',share, False) if password else False expire = get_or_no('expire',share, "-") table.append(( share['name'], share['path']+"/", public, password, upload, overwrite, direct, expire )) 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']) folders = sorted(os.listdir(data_folder)) table = [] table.append( ('Path','Share','Size','Unit') ) for folder in folders: full_path = os.path.join(data_folder, folder) if not os.path.isdir(full_path): continue share_name = "[unused by any share]" 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 (size_num, size_unit) = file_size_human(get_folder_size( full_path )).split(" ") table.append(( folder, share_name, size_num, size_unit )) print(tabulate(table, headers = "firstrow")) def add_share(shares, config, opts): share = { 'name': opts.name, 'path': opts.path, 'public': opts.public, 'upload': opts.upload, 'overwrite': opts.overwrite, 'direct_links': opts.direct, } if opts.plain: share.update({ 'pass_plain': opts.plain }) if opts.hashed: share.update({ 'pass_hash': hashlib.sha1(opts.hashed).hexdigest() }) if opts.expire: try: date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M") except ValueError as e: print(e) sys.exit(1) share.update({ 'expire': opts.expire }) if opts.insert: 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, )) else: print("Share not saved anywhere. Save with -i") print(json.dumps(share, indent = 2, sort_keys = True)) 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)) 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)) 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("Link to list contents of the share:") print("%s/list/%s"%( config['public_url'], share['name'] )) return if not 'pass_hash' in share: print("REST API enabled only if pass_hash is set for share") sys.exit(1) if opts.type == "login": print("Link to automatically login in the share:") print("%s/list/%s/%s"%( config['public_url'], share['name'], share['pass_hash'] )) elif opts.type == "upload": 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("curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%( config['public_url'], share['name'], share['pass_hash'] )) elif opts.type == "download": 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 sorted(os.listdir(share_path)): print("%s/download/%s/%s/%s"%( config['public_url'], share['name'], share['pass_hash'], filename )) elif opts.type == "direct": 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 sorted(os.listdir(share_path)): print("%s/direct/%s/%s/%s"%( config['public_url'], share['name'], get_direct_token(share,filename), filename )) elif opts.type == "zip": print("ZIP download:") print("%s/zip/%s/%s"%( config['public_url'], share['name'], share['pass_hash'] )) 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('-P', action="store_true", dest="show_password", default = False, help = "Display hashed passwords") ## list folders parser_folders = subparsers.add_parser('folders', help = "List folders and share names") ## Remove parser_remove = subparsers.add_parser('remove', help = "Remove a share") parser_remove.add_argument(dest="name") ## 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('-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") 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", dest="plain", default = False) parser_add.add_argument('--pass-hash', action="store", dest="hashed", default = False) 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('-i','--insert', action="store_true", dest="insert", default = False, help = "Insert new share directly in 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'] ) 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 == 'remove': remove_share(shares,config,opts) elif opts.subparser_name == 'add': add_share(shares,config,opts) elif opts.subparser_name == 'rest': print_rest_api(shares,config,opts)