diff --git a/flit.py b/flit.py index 6310140..5d43ced 100755 --- a/flit.py +++ b/flit.py @@ -1,5 +1,4 @@ -#!/usr/bin/python3 - +#!/usr/bin/env python3 from datetime import datetime, timedelta import argparse import os @@ -9,10 +8,30 @@ import string from urllib.parse import quote import json +"""FLIT: Fluttering folders for simple file sharing. + +The script maintains a set of folders, named so that they are hard to guess, +and contents are deleted after expiration date. + +Attributes: + CONFIG (str): Configuration file name stored in each shared folder. + +""" + CONFIG = ".flit.json" def parse_opts(): + """Options parser + + Args: + None + + Returns: + Namespace: Options from the command line + + """ + class _HelpAction(argparse._HelpAction): def __call__(self, parser, namespace, values, option_string=None): parser.print_help() @@ -94,10 +113,30 @@ def parse_opts(): def random_char(): + """Random character generator + + Args: + None + + Returns: + str: A single uppercase letter or number + + """ return random.choice(string.ascii_uppercase + string.digits) def random_name(): + """Random unique name for a new share. Share names follow the syntax [NUM-ABC-DEF], + Where NUM is a 3 digit growing number (to help navigating new folders), but the + rest are random, to ensure secrecy. + + Args: + None + + Returns: + str: New folder name + + """ while True: existing_names = [c["name"] for c in get_folders()] index = 0 @@ -118,6 +157,17 @@ def random_name(): def create_new(p, days, description): + """Creates a new folder, and stores configuration + + Args: + p (str): Share name + days (int): Days to store the folder + description (str): Description of the fileshare + + Returns: + None + + """ os.mkdir(p) now = datetime.now() del_delta = timedelta(days=days) @@ -130,6 +180,17 @@ def create_new(p, days, description): def copy_files(cwd, new_name, files): + """Copy files to a new share + + Args: + c (str): Working directory when the script was started + new_name (str): Name of the new share + files (list): List of paths to copy + + Returns: + None + + """ target = os.path.abspath(new_name) for f in opts.files: print(f"Copying: {f}") @@ -150,9 +211,17 @@ def copy_files(cwd, new_name, files): ) - def get_stats(p): - # TODO: named tuple + """Get share properties + + Args: + p (str): Name of the share + + Returns: + Dict: Share properties + + """ + config = read_config(p) del_time = datetime.fromisoformat(config["delete_time"]) @@ -171,6 +240,16 @@ def get_stats(p): def get_sub_files(p): + """Get 1st level files in a share + + Args: + p (str): Name of the share + + Returns: + List: Folders and Files in the share + + """ + file_list = sorted([x for x in os.listdir(p) if not x.startswith(".")]) dir_list = [x + "/" for x in file_list if os.path.isdir(os.path.join(p, x))] file_list = [x for x in file_list if os.path.isfile(os.path.join(p, x))] @@ -178,15 +257,38 @@ def get_sub_files(p): def get_folders(): + """Get share properties for all the shares + + Args: + None: + + Returns: + List: List that contain `get_stats(p)` for every share + + """ + dir_list = [p for p in os.listdir(".") if os.path.exists(os.path.join(p, CONFIG))] configs = [get_stats(p) for p in dir_list] configs.sort(key=lambda c: c["created"], reverse=True) return configs -def list_folders(root_folder, verbose=False, filter_name=None): +def list_folders(root_url, verbose=False, filter_name=None): + """Show folder list + + Args: + root_url (str): URL to root of the share system + verbose (bool): If set true, displays folder contents + filter_name (str): If set, display only one folder matching the name + + Returns: + None + + """ + folders = get_folders() - print("Folder Created ToDelete InDays") + header_format = "{:" + str(12 + len(root_url)) + "} {} {} {} {}" + print(header_format.format("URL","Created","ToDelete","InDays","Description")) for c in folders: if filter_name is not None: if filter_name != c["name"]: @@ -194,7 +296,7 @@ def list_folders(root_folder, verbose=False, filter_name=None): due = "*" if c["due"] else " " print( "{}/{}/ {} {} {: 4d}d{} {}".format( - root_folder, + root_url, c["name"], c["created"].isoformat()[0:10], c["delete_time"].isoformat()[0:10], @@ -206,10 +308,19 @@ def list_folders(root_folder, verbose=False, filter_name=None): if verbose: sub_files = get_sub_files(c["name"]) for sp in sub_files: - print(" {}/{}/{}".format(root_folder, c["name"], quote(sp, "/"))) + print(" {}/{}/{}".format(root_url, c["name"], quote(sp, "/"))) def del_due_folders(): + """Delete folders where due date has passed + + Args: + None + + Returns: + None + + """ folders = get_folders() for c in folders: if c["due"]: @@ -218,11 +329,37 @@ def del_due_folders(): def read_config(p): + """Read the share configuration file + + Args: + p (str): Share name + + Returns: + Dict: Share config + + Example: + { + "created": "2020-12-28T23:04:07.275200", + "delete_time": "2020-12-29T23:04:07.275200", + "description": "Test share" + } + + """ with open(os.path.join(p, CONFIG), "rt") as fp: return json.load(fp) def write_config(p, config): + """Write the share configuration file + + Args: + p (str): Share name + config (Dict): Share configuration + + Returns: + None + """ + with open(os.path.join(p, CONFIG), "wt") as fp: return json.dump(config, fp, indent=2, sort_keys=True) @@ -232,6 +369,8 @@ if __name__ == "__main__": cwd = os.getcwd() os.chdir(os.path.dirname(__file__)) if opts.command == "add": + """ Add a new share """ + new_name = random_name() create_new(new_name, opts.days, opts.description) print(os.path.abspath(new_name)) @@ -240,7 +379,11 @@ if __name__ == "__main__": list_folders(opts.root, verbose=True, filter_name=new_name) if opts.command == "list" or opts.command is None: + """ List shares """ + list_folders(opts.root, verbose=opts.verbose) if opts.command == "del": + """ Delete due shares """ + del_due_folders()