import os import sys import json import time import bcrypt import argparse import sqlite3 from werkzeug.utils import secure_filename from datetime import datetime, timedelta from utils import read_config ENTRY = {"expires": "never", "password": None} CONFIGS = os.getenv("CONFIG_FOLDER") FOLDERS = os.getenv("STATIC_FOLDER") DATABASE = os.getenv("DATABASE", None) SCHEMA = """CREATE TABLE IF NOT EXISTS sessions( ID INTEGER PRIMARY KEY AUTOINCREMENT, folder TEXT NOT NULL, ip TEXT NOT NULL, token TEXT NOT NULL, expire INTEGER NOT NULL );""" def get_opts(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") sub_add = subparsers.add_parser("add") sub_remove = subparsers.add_parser("remove") sub_edit = subparsers.add_parser("edit") sub_list = subparsers.add_parser("list") sub_session_list = subparsers.add_parser("sessions-list") sub_session_clean = subparsers.add_parser( "sessions-clean", help="Clean out session DB" ) sub_session_remove = subparsers.add_parser( "sessions-remove", help="Terminates all sessions" ) for p in (sub_add, sub_remove, sub_edit): p.add_argument("folder", action="store", help="Folder name to share") for p in (sub_add, sub_edit): p.add_argument( "--expires", action="store", help="Share expires in days, or 'never'", default=None, ) p.add_argument( "--password", action="store", help="Set password", default=None ) args = parser.parse_args() return args def manager(): opts = get_opts() if opts.command == None or opts.command == "list": shares_list() if opts.command == "add": share_add(opts) if opts.command == "remove": share_remove(opts) if opts.command == "edit": share_edit(opts) if opts.command == "sessions-list": session_list() if opts.command == "sessions-remove": session_remove() if opts.command == "sessions-clean": session_clean() def load_config(name): config = read_config(name) return config def save_config(path, config): with open(path, "wt") as fp: save_config = ENTRY.copy() for key in save_config: save_config[key] = config[key] return json.dump(save_config, fp, indent=2, sort_keys=True) def share_oneliner(entry): return f"{(entry['name']+'/').ljust(15)} expires: {entry['expires']}" def shares_list(): print("Shared folders:") for c in sorted(os.listdir(CONFIGS)): if c.endswith(".json"): entry = load_config(c[0:-5]) print(share_oneliner(entry)) def share_add(opts): entry = ENTRY.copy() if opts.folder != secure_filename(opts.folder): raise ValueError(f"Folder '{opts.folder}' is not a safe filename") if opts.expires: entry["expires"] = ( (datetime.now() + timedelta(days=float(opts.expires))) .replace(microsecond=0) .isoformat() ) if not opts.password: raise ValueError("Password required") (pw, _) = hash_password(opts.password) entry["password"] = pw share_folder = os.path.join(FOLDERS, opts.folder) share_config = os.path.join(CONFIGS, f"{opts.folder}.json") if os.path.exists(share_config): raise FileExistsError("Configuration already exists. Edit with `edit`") save_config(share_config, entry) if not os.path.exists(share_folder): os.mkdir(share_folder) print(f"Added:\n{share_oneliner(load_config(opts.folder))}") def share_remove(opts): share_folder = os.path.join(FOLDERS, opts.folder) share_config = os.path.join(CONFIGS, f"{opts.folder}.json") if not os.path.exists(share_config): raise FileNotFoundError("Configuration doesn't exist.") else: print(f"Removing configuration {share_config}") os.remove(share_config) if os.path.exists(share_folder): if len(os.listdir(share_folder)) > 0: print(f"Not removing folder {share_folder}, contains data") else: print(f"Removing empty folder {share_folder}") os.rmdir(share_folder) def share_edit(opts): share_config = os.path.join(CONFIGS, f"{opts.folder}.json") if not os.path.exists(share_config): raise FileNotFoundError("Configuration doesn't exist.") entry = load_config(opts.folder) if opts.expires: print(f"Updating expiry date: {opts.expires}") if opts.expires == "never": entry["expires"] = opts.expires else: entry["expires"] = ( (datetime.now() + timedelta(days=float(opts.expires))) .replace(microsecond=0) .isoformat() ) if opts.password: print("Updating password") (pw, _) = hash_password(opts.password) entry["password"] = pw save_config(share_config, entry) print(f"Added:\n{share_oneliner(load_config(opts.folder))}") def session_get_database(): return sqlite3.connect(DATABASE) def session_create_database(): args = tuple() db = session_get_database() cur = db.execute(SCHEMA, args) db.commit() cur.close() def session_list(): query = "SELECT folder, expire, token, ip FROM sessions" args = tuple() cur = session_get_database().execute(query, args) print(f"{'Share'.ljust(15)} {'Expires'.ljust(19)} IP") for session in cur.fetchall(): d = datetime.fromtimestamp(session[1]) print(f"{session[0].ljust(15)} {d} {session[3]}") cur.close() def session_remove(): query = "DELETE FROM sessions" args = tuple() db = session_get_database() cur = db.execute(query, args) db.commit() cur.close() def session_clean(): query = "DELETE FROM sessions WHERE expire < ?" args = (int(time.time()),) db = session_get_database() cur = db.execute(query, args) db.commit() cur.close() def hash_password(pw): pw = pw.encode("utf-8") salt = bcrypt.gensalt() hashed = bcrypt.hashpw(pw, salt) return hashed.decode(), salt.decode() if __name__ == "__main__": manager()