403 lines
9.7 KiB
Python
Executable File
403 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from datetime import datetime, timedelta
|
|
import argparse
|
|
import os
|
|
import random
|
|
import shutil
|
|
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()
|
|
print("")
|
|
# retrieve subparsers from parser
|
|
subparsers_actions = [
|
|
action
|
|
for action in parser._actions
|
|
if isinstance(action, argparse._SubParsersAction)
|
|
]
|
|
for subparsers_action in subparsers_actions:
|
|
# get all subparsers and print help
|
|
for choice, subparser in subparsers_action.choices.items():
|
|
print("Command: {}".format(choice))
|
|
print(subparser.format_help())
|
|
|
|
parser.exit()
|
|
|
|
parser = argparse.ArgumentParser(add_help=False)
|
|
parser.add_argument(
|
|
"--help", "-h", action=_HelpAction, help="Show this help message and exit"
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
action="store_true",
|
|
dest="verbose",
|
|
default=False,
|
|
help="Increase verbosity",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--root",
|
|
"-r",
|
|
action="store",
|
|
dest="root",
|
|
type=str,
|
|
default="",
|
|
help="Root address for printing URLS",
|
|
)
|
|
parser.add_argument(
|
|
"--home",
|
|
action="store",
|
|
dest="home",
|
|
type=str,
|
|
default=None,
|
|
help="Home folder for flit. This is where folders are created, and --root should point to in the filesystem. Defaults to script location",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command", help="Command defaults to add")
|
|
add_parser = subparsers.add_parser("add", add_help=False)
|
|
add_parser.add_argument(
|
|
"-d",
|
|
action="store",
|
|
type=int,
|
|
help="Days to keep files",
|
|
default=30,
|
|
dest="days",
|
|
)
|
|
add_parser.add_argument(
|
|
"-m",
|
|
action="store",
|
|
type=str,
|
|
help="Describe share",
|
|
default="-",
|
|
dest="description",
|
|
)
|
|
add_parser.add_argument(
|
|
"files",
|
|
action="store",
|
|
type=str,
|
|
help="Copy files/folders under the new share",
|
|
default=[],
|
|
nargs="*",
|
|
)
|
|
|
|
list_parser = subparsers.add_parser("list", add_help=False)
|
|
list_parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
action="store_true",
|
|
dest="verbose",
|
|
default=False,
|
|
help="Print individual files too",
|
|
)
|
|
del_parser = subparsers.add_parser("del", add_help=False)
|
|
return parser.parse_args()
|
|
|
|
|
|
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
|
|
for existing in existing_names:
|
|
try:
|
|
e_index = int(existing[0:3])
|
|
index = max(e_index + 1, index)
|
|
except:
|
|
pass
|
|
name = "{:03d}-{}-{}".format(
|
|
index,
|
|
"".join([random_char() for x in range(3)]),
|
|
"".join([random_char() for x in range(3)]),
|
|
)
|
|
if not os.path.exists(name):
|
|
break
|
|
return 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)
|
|
del_time = now + del_delta
|
|
config = {}
|
|
config["description"] = description
|
|
config["delete_time"] = del_time.isoformat()
|
|
config["created"] = now.isoformat()
|
|
write_config(p, config)
|
|
|
|
|
|
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}")
|
|
source = os.path.join(cwd, f)
|
|
if source.endswith("/"):
|
|
source = source[0:-1]
|
|
base = os.path.basename(source)
|
|
if not os.path.exists(source):
|
|
print("Path does not exist")
|
|
continue
|
|
if os.path.isfile(source):
|
|
shutil.copy2(source, target)
|
|
else:
|
|
shutil.copytree(
|
|
source,
|
|
os.path.join(target, base),
|
|
symlinks=True,
|
|
)
|
|
|
|
|
|
def get_stats(p):
|
|
"""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"])
|
|
now = datetime.now()
|
|
mtime = datetime.fromisoformat(config["created"])
|
|
|
|
to_del_time = del_time - now
|
|
is_due = now > del_time
|
|
config["delete_time"] = del_time
|
|
config["created"] = mtime
|
|
config["name"] = p
|
|
config["to_deletion"] = to_del_time
|
|
config["due"] = is_due
|
|
|
|
return config
|
|
|
|
|
|
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))]
|
|
return dir_list + file_list
|
|
|
|
|
|
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_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()
|
|
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"]:
|
|
continue
|
|
due = "*" if c["due"] else " "
|
|
print(
|
|
"{}/{}/ {} {} {: 4d}d{} {}".format(
|
|
root_url,
|
|
c["name"],
|
|
c["created"].isoformat()[0:10],
|
|
c["delete_time"].isoformat()[0:10],
|
|
c["to_deletion"].days,
|
|
due,
|
|
c["description"],
|
|
)
|
|
)
|
|
if verbose:
|
|
sub_files = get_sub_files(c["name"])
|
|
for sp in sub_files:
|
|
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"]:
|
|
print("Deleting {}".format(c["name"]))
|
|
shutil.rmtree(c["name"])
|
|
|
|
|
|
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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
opts = parse_opts()
|
|
cwd = os.getcwd()
|
|
if opts.home == None:
|
|
os.chdir(os.path.dirname(__file__))
|
|
else:
|
|
os.chdir(opts.home)
|
|
|
|
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))
|
|
print("")
|
|
copy_files(cwd, new_name, opts.files)
|
|
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()
|