#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals

"""copyparty: http file sharing hub (py2/py3)"""
__author__ = "ed <copyparty@ocv.me>"
__copyright__ = 2019
__license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/"

import argparse
import base64
import locale
import os
import re
import select
import socket
import sys
import threading
import time
import traceback
import uuid

from .__init__ import (
    ANYWIN,
    CORES,
    EXE,
    MACOS,
    PY2,
    PY36,
    VT100,
    WINDOWS,
    E,
    EnvParams,
    unicode,
)
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cfg import flagcats, onedash
from .svchub import SvcHub
from .util import (
    APPLESAN_TXT,
    BAD_BOTS,
    DEF_EXP,
    DEF_MTE,
    DEF_MTH,
    HAVE_IPV6,
    IMPLICATIONS,
    JINJA_VER,
    MIKO_VER,
    MIMES,
    PARTFTPY_VER,
    PY_DESC,
    PYFTPD_VER,
    RAM_AVAIL,
    RAM_TOTAL,
    RE_ANSI,
    SQLITE_VER,
    UNPLICATIONS,
    URL_BUG,
    URL_PRJ,
    Daemon,
    align_tab,
    b64enc,
    ctypes,
    dedent,
    expand_osenv_c,
    expand_osenv_cs,
    expand_osenv_noop,
    expand_osenv_s,
    has_resource,
    list_ips,
    list_nics,
    load_resource,
    lprint,
    min_ex,
    pybin,
    read_utf8,
    termsize,
    wk32,
    wrap,
)

if PY2:
    range = xrange  # type: ignore

try:
    if os.environ.get("PRTY_NO_TLS"):
        raise Exception()

    HAVE_SSL = True
    import ssl
except:
    HAVE_SSL = False

u = unicode
zsid = uuid.uuid4().urn[4:]

CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
    CFG_DEF.pop()


class RiceFormatter(argparse.HelpFormatter):
    def __init__(self, *args , **kwargs )  :
        if PY2:
            kwargs["width"] = termsize()[0]

        super(RiceFormatter, self).__init__(*args, **kwargs)

    def _get_help_string(self, action )  :
        """
        same as ArgumentDefaultsHelpFormatter(HelpFormatter)
        except the help += [...] line now has colors
        """
        fmt = "\033[36m (default: \033[35m%(default)s\033[36m)\033[0m"
        if not VT100:
            fmt = " (default: %(default)s)"

        ret = unicode(action.help)
        if "%(default)" not in ret:
            if action.default is not argparse.SUPPRESS:
                defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
                if action.option_strings or action.nargs in defaulting_nargs:
                    ret += fmt

        if not VT100:
            ret = re.sub("\033\\[[0-9;]+m", "", ret)

        return ret  # type: ignore

    def _fill_text(self, text , width , indent )  :
        """same as RawDescriptionHelpFormatter(HelpFormatter)"""
        return "".join(indent + line + "\n" for line in text.splitlines())

    def __add_whitespace(self, idx , iWSpace , text )  :
        return (" " * iWSpace) + text if idx else text

    def _split_lines(self, text , width )  :
        # https://stackoverflow.com/a/35925919
        textRows = text.splitlines()
        ptn = re.compile(r"\s*[0-9\-]{0,}\.?\s*")
        for idx, line in enumerate(textRows):
            search = ptn.search(line)
            if not line.strip():
                textRows[idx] = " "
            elif search:
                lWSpace = search.end()
                lines = [
                    self.__add_whitespace(i, lWSpace, x)
                    for i, x in enumerate(wrap(line, width, width - 1))
                ]
                textRows[idx] = lines  # type: ignore

        return [item for sublist in textRows for item in sublist]


class Dodge11874(RiceFormatter):
    def __init__(self, *args , **kwargs )  :
        kwargs["width"] = 9003
        super(Dodge11874, self).__init__(*args, **kwargs)


class BasicDodge11874(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    def __init__(self, *args , **kwargs )  :
        kwargs["width"] = 9003
        super(BasicDodge11874, self).__init__(*args, **kwargs)


def warn(msg )  :
    lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))


def init_E(EE )  :
    # some cpython alternatives (such as pyoxidizer) can
    # __init__ several times, so do expensive stuff here

    E = EE  # pylint: disable=redefined-outer-name

    def get_unixdir()   :
        paths    = [
            (os.environ.get, "XDG_CONFIG_HOME"),
            (os.path.expanduser, "~/.config"),
            (os.environ.get, "TMPDIR"),
            (os.environ.get, "TEMP"),
            (os.environ.get, "TMP"),
            (unicode, "/tmp"),
        ]
        errs = []
        for npath, (pf, pa) in enumerate(paths):
            priv = npath < 2  # private/trusted location
            ram = npath > 1  # "nonvolatile"; not semantically same as `not priv`
            p = ""
            try:
                p = pf(pa)
                if not p or p.startswith("~"):
                    continue

                p = os.path.normpath(p)
                mkdir = not os.path.isdir(p)
                if mkdir:
                    os.mkdir(p, 0o700)

                p = os.path.join(p, "copyparty")
                if not priv and os.path.isdir(p):
                    uid = os.geteuid()
                    if os.stat(p).st_uid != uid:
                        p += ".%s" % (uid,)
                        if os.path.isdir(p) and os.stat(p).st_uid != uid:
                            raise Exception("filesystem has broken unix permissions")
                try:
                    os.listdir(p)
                except:
                    os.mkdir(p, 0o700)

                if ram:
                    t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
                    errs.append(t % (pa, p))
                elif mkdir:
                    t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
                    errs.append(t % (pa, p, " instead" if npath else "", pa))
                elif errs:
                    errs.append("Using %s/copyparty [%s] instead" % (pa, p))

                if errs:
                    warn(". ".join(errs))

                return p, priv
            except Exception as ex:
                if p:
                    t = "Unable to store config in %s [%s] due to %r"
                    errs.append(t % (pa, p, ex))

        t = "could not find a writable path for runtime state:\n> %s"
        raise Exception(t % ("\n> ".join(errs)))

    E.mod = os.path.dirname(os.path.realpath(__file__))
    if E.mod.endswith("__init__"):
        E.mod = os.path.dirname(E.mod)
    E.mod_ = os.path.join(E.mod, "")

    try:
        p = os.environ.get("XDG_CONFIG_HOME")
        if not p:
            raise Exception()
        if p.startswith("~"):
            p = os.path.expanduser(p)
        p = os.path.abspath(os.path.realpath(p))
        p = os.path.join(p, "copyparty")
        if not os.path.isdir(p):
            os.mkdir(p, 0o700)
        os.listdir(p)
    except:
        p = ""

    if p:
        E.cfg = p
    elif sys.platform == "win32":
        bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
        E.cfg = os.path.normpath(bdir + "/copyparty")
    elif sys.platform == "darwin":
        E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
    else:
        E.cfg, E.scfg = get_unixdir()

    E.cfg = E.cfg.replace("\\", "/")
    try:
        bos.makedirs(E.cfg, bos.MKD_700)
    except:
        if not os.path.isdir(E.cfg):
            raise


def get_srvname(verbose)  :
    try:
        ret  = unicode(socket.gethostname()).split(".")[0]
    except:
        ret = ""

    if ret not in ["", "localhost"]:
        return ret

    fp = os.path.join(E.cfg, "name.txt")
    if verbose:
        lprint("using hostname from {}\n".format(fp))
    try:
        return read_utf8(None, fp, True).strip()
    except:
        ret = ""
        namelen = 5
        while len(ret) < namelen:
            ret += base64.b32encode(os.urandom(4))[:7].decode("utf-8").lower()
            ret = re.sub("[234567=]", "", ret)[:namelen]
        with open(fp, "wb") as f:
            f.write(ret.encode("utf-8") + b"\n")
        return ret


def get_salt(name , nbytes )  :
    fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
    try:
        return read_utf8(None, fp, True).strip()
    except:
        ret = b64enc(os.urandom(nbytes))
        with open(fp, "wb") as f:
            f.write(ret + b"\n")
        return ret.decode("utf-8")


def ensure_locale()  :
    if ANYWIN and PY2:
        return  # maybe XP, so busted 65001

    safe = "en_US.UTF-8"
    for x in [
        safe,
        "English_United States.UTF8",
        "English_United States.1252",
    ]:
        try:
            locale.setlocale(locale.LC_ALL, x)
            if x != safe:
                lprint("Locale: {}\n".format(x))
            return
        except:
            continue

    t = "setlocale {} failed,\n  sorting and dates might get funky\n"
    warn(t.format(safe))


def ensure_webdeps()  :
    if has_resource(E, "web/deps/mini-fa.woff"):
        return

    t = """could not find webdeps;
  if you are running the sfx, or exe, or pypi package, or docker image,
  then this is a bug! Please let me know so I can fix it, thanks :-)
  %s

  however, if you are a dev, or running copyparty from source, and you want
  full client functionality, you will need to build or obtain the webdeps:
  %s/blob/hovudstraum/docs/devnotes.md#building
    """
    warn(t % (URL_BUG, URL_PRJ))


def configure_ssl_ver(al )  :
    def terse_sslver(txt )  :
        txt = txt.lower()
        for c in ["_", "v", "."]:
            txt = txt.replace(c, "")

        return txt.replace("tls10", "tls1")

    # oh man i love openssl
    # check this out
    # hold my beer
    ptn = re.compile(r"^OP_NO_(TLS|SSL)v")
    sslver = terse_sslver(al.ssl_ver).split(",")
    flags = [k for k in ssl.__dict__ if ptn.match(k)]
    # SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3
    if "help" in sslver:
        avail1 = [terse_sslver(x[6:]) for x in flags]
        avail = " ".join(sorted(avail1) + ["all"])
        lprint("\navailable ssl/tls versions:\n  " + avail)
        sys.exit(0)

    al.ssl_flags_en = 0
    al.ssl_flags_de = 0
    for flag in sorted(flags):
        ver = terse_sslver(flag[6:])
        num = getattr(ssl, flag)
        if ver in sslver:
            al.ssl_flags_en |= num
        else:
            al.ssl_flags_de |= num

    if sslver == ["all"]:
        x = al.ssl_flags_en
        al.ssl_flags_en = al.ssl_flags_de
        al.ssl_flags_de = x

    for k in ["ssl_flags_en", "ssl_flags_de"]:
        num = getattr(al, k)
        lprint("{0}: {1:8x} ({1})".format(k, num))

    # think i need that beer now


def configure_ssl_ciphers(al )  :
    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    if al.ssl_ver:
        ctx.options &= ~al.ssl_flags_en
        ctx.options |= al.ssl_flags_de

    is_help = al.ciphers == "help"

    if al.ciphers and not is_help:
        try:
            ctx.set_ciphers(al.ciphers)
        except:
            lprint("\n\033[1;31mfailed to set ciphers\033[0m\n")

    if not hasattr(ctx, "get_ciphers"):
        lprint("cannot read cipher list: openssl or python too old")
    else:
        ciphers = [x["description"] for x in ctx.get_ciphers()]
        lprint("\n  ".join(["\nenabled ciphers:"] + align_tab(ciphers) + [""]))

    if is_help:
        sys.exit(0)


def expand_cvars(argv)  :
    n = 0
    for v in argv:
        if "=" in v:
            a, b = v.split("=", 1)
            v = "%s=%s" % (a, os.path.expanduser(expand_osenv_c(b)))
        else:
            v = os.path.expanduser(expand_osenv_c(v))
        argv[n] = v
        n += 1
    return argv


def args_from_cfg(cfg_path )  :
    lines  = []
    expand_config_file(None, expand_osenv_c, lines, cfg_path, "")
    lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")

    ret  = []
    skip = True
    for ln in lines:
        sn = ln.split("  #")[0].strip()
        if sn.startswith("["):
            skip = True
        if sn.startswith("[global]"):
            skip = False
            continue
        if skip or not sn.split("#")[0].strip():
            continue
        for k, v in split_cfg_ln(sn).items():
            k = k.lstrip("-")
            if not k:
                continue
            prefix = "-" if k in onedash else "--"
            if v is True:
                ret.append(prefix + k)
            else:
                ret.append(prefix + k + "=" + v)

    return expand_cvars(ret)


def expand_cfg(argv)  :
    argv = expand_cvars(argv)

    if CFG_DEF:
        supp = args_from_cfg(CFG_DEF[0])
        argv = argv[:1] + supp + argv[1:]

    n = 0
    while n < len(argv):
        v1 = argv[n]
        v1v = v1[2:].lstrip("=")
        try:
            v2 = argv[n + 1]
        except:
            v2 = ""

        n += 1
        if v1 == "-c" and v2 and os.path.isfile(v2):
            n += 1
            argv = argv[:n] + args_from_cfg(v2) + argv[n:]
        elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
            argv = argv[:n] + args_from_cfg(v1v) + argv[n:]
    return argv


def quotecheck(al):
    for zs1, zco in vars(al).items():
        zsl = [u(x) for x in zco] if isinstance(zco, list) else [u(zco)]
        for zs2 in zsl:
            zs2 = zs2.strip()
            zs3 = zs2.strip("\"'")
            if zs2 == zs3 or len(zs2) - len(zs3) < 2:
                continue
            if al.c:
                t = "found the following global-config:   %s: %s\n values should not be quoted; did you mean:   %s: %s"
            else:
                t = 'found the following config-option:  "--%s=%s"\n values should not be quoted; did you mean:  "--%s=%s"'
            warn(t % (zs1, zs2, zs1, zs3))


def sighandler(sig  = None, frame  = None)  :
    msg = [""] * 5
    for th in threading.enumerate():
        stk = sys._current_frames()[th.ident]  # type: ignore
        msg.append(str(th))
        msg.extend(traceback.format_stack(stk))

    msg.append("\n")
    print("\n".join(msg))


def disable_quickedit()  :
    if not ctypes:
        raise Exception("no ctypes")

    import atexit
    from ctypes import wintypes

    def ecb(ok , fun , args )  :
        if not ok:
            err  = ctypes.get_last_error()  # type: ignore
            if err:
                raise ctypes.WinError(err)  # type: ignore
        return args

    if PY2:
        wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)

    k32 = wk32
    k32.GetStdHandle.errcheck = ecb  # type: ignore
    k32.GetConsoleMode.errcheck = ecb  # type: ignore
    k32.SetConsoleMode.errcheck = ecb  # type: ignore
    k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD)
    k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD)

    def cmode(out , mode  = None)  :
        h = k32.GetStdHandle(-11 if out else -10)
        if mode:
            return k32.SetConsoleMode(h, mode)  # type: ignore

        cmode = wintypes.DWORD()
        k32.GetConsoleMode(h, ctypes.byref(cmode))
        return cmode.value

    # disable quickedit
    mode = orig_in = cmode(False)
    quickedit = 0x40
    extended = 0x80
    mask = quickedit + extended
    if mode & mask != extended:
        atexit.register(cmode, False, orig_in)
        cmode(False, mode & ~mask | extended)

    # enable colors in case the os.system("rem") trick ever stops working
    if VT100:
        mode = orig_out = cmode(True)
        if mode & 4 != 4:
            atexit.register(cmode, True, orig_out)
            cmode(True, mode | 4)


def sfx_tpoke(top ):
    if os.environ.get("PRTY_NO_TPOKE"):
        return

    files = [top] + [
        os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df
    ]
    while True:
        t = int(time.time())
        for f in list(files):
            try:
                os.utime(f, (t, t))
            except Exception as ex:
                lprint("<TPOKE> [%s] %r" % (f, ex))
                files.remove(f)

        time.sleep(78123)


def showlic()  :
    try:
        with load_resource(E, "res/COPYING.txt") as f:
            buf = f.read()
    except:
        buf = b""

    if buf:
        print(buf.decode("utf-8", "replace"))
    else:
        print("no relevant license info to display")
        return


def get_sects():
    return [
        [
            "bind",
            "configure listening",
            dedent(
                """
            \033[33m-i\033[0m takes a comma-separated list of interfaces to listen on;
            IP-addresses, unix-sockets, and/or open file descriptors

            the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses

            \033[32m-i 0.0.0.0\033[0m    listens on all IPv4 NICs/subnets
            \033[32m-i 127.0.0.1\033[0m  listens on IPv4 localhost only
            \033[32m-i 127.1\033[0m      listens on IPv4 localhost only
            \033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1

            \033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
            the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m

            when running behind a reverse-proxy, it's recommended to
            use unix-sockets for improved performance and security;

            \033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
            \033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
            only accessible to members of the \033[33mwww\033[0m group.
            This is the best approach. Alternatively,

            \033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
            can access it; bad unless it's inside a restricted folder

            \033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission
            (usually \033[33m0600\033[0m) and the same user/group as copyparty

            \033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3

            \033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs

            \033[33m--list-nics\033[0m shows all network adapters (also offline ones);
            \033[33m--list-ips\033[0m shows all LAN IPs
            """
            ),
        ],
        [
            "accounts",
            "accounts and volumes",
            dedent(
                """
            -a takes username:password,
            -v takes src:dst:\033[33mperm\033[0m1:\033[33mperm\033[0m2:\033[33mperm\033[0mN:\033[32mvolflag\033[0m1:\033[32mvolflag\033[0m2:\033[32mvolflag\033[0mN:...
                * "\033[33mperm\033[0m" is "permissions,username1,username2,..."
                * "\033[32mvolflag\033[0m" is config flags to set on this volume

            --grp takes groupname:username1,username2,...
            and groupnames can be used instead of usernames in -v
            by prefixing the groupname with @

            list of permissions:
              "r" (read):   list folder contents, download files
              "w" (write):  upload files; need "r" to see the uploads
              "m" (move):   move files and folders; need "w" at destination
              "d" (delete): permanently delete files and folders
              "g" (get):    download files, but cannot see folder contents
              "G" (upget):  "get", but can see filekeys of their own uploads
              "h" (html):   "get", but folders return their index.html
              "." (dots):   user can ask to show dotfiles in listings
              "a" (admin):  can see uploader IPs, config-reload
              "A" ("all"):  same as "rwmda." (read/write/move/delete/admin/dotfiles)

            too many volflags to list here, see --help-flags

            example:\033[35m
              -a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe  \033[36m
              mount current directory at "/" with
               * r (read-only) for everyone
               * rw (read+write) for ed
              mount ../inc at "/dump" with
               * w (write-only) for everyone
               * rw (read+write) for ed
               * reject duplicate files  \033[0m

            if no accounts or volumes are configured,
            current folder will be read/write for everyone

            the group \033[33m@acct\033[0m will always have every user with an account
            (the name of that group can be changed with \033[32m--grp-all\033[0m)

            to hide a volume from authenticated users, specify \033[33m*,-@acct\033[0m
            to subtract \033[33m@acct\033[0m from \033[33m*\033[0m (can subtract users from groups too)

            consider the config file for more flexible account/volume management,
            including dynamic reload at runtime (and being more readable w)

            see \033[32m--help-auth\033[0m for ways to provide the password in requests;
            see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
            """
            ),
        ],
        [
            "auth",
            "how to login from a client",
            dedent(
                """
            different ways to provide the password so you become authenticated:

            login with the ui:
              go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there

            send the password in the '\033[36mPW\033[0m' http-header:
              \033[36mPW: \033[35mhunter2\033[0m
            or if you have \033[33m--usernames\033[0m enabled,
              \033[36mPW: \033[35med:hunter2\033[0m

            send the password in the URL itself:
              \033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
            or if you have \033[33m--usernames\033[0m enabled,
              \033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m

            use basic-authentication:
              \033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
            which should be the same as this header:
              \033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
            """
            ),
        ],
        [
            "auth-ord",
            "authentication precedence",
            dedent(
                """
            \033[33m--auth-ord\033[0m is a comma-separated list of auth options
            (one or more of the [\033[35moptions\033[0m] below); first one wins

            [\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
              or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
              (see \033[33m--help-auth\033[0m)

            [\033[35midp\033[0m] is a username provided in the http-request-header
              defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
              provided by an authentication middleware such as
              authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)

            [\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
            [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
            [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]

            [\033[35mipu\033[0m] is a mapping from an IP-address to a username,
              auto-authing that client-IP to that account
              (see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)

            NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
              it may still be enabled and can still take effect if
              none of the other alternatives identify the user

            NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST

            NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
              will be hidden when any idp feature is enabled
            """
            ),
        ],
        [
            "flags",
            "list of volflags",
            dedent(
                """
            volflags are appended to volume definitions, for example,
            to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
              \033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub\033[0m

            if global config defines a volflag for all volumes,
            you can unset it for a specific volume with -flag
            """
            ).rstrip()
            + build_flags_desc(),
        ],
        [
            "handlers",
            "use plugins to handle certain events",
            dedent(
                """
            usually copyparty returns a \033[33m404\033[0m if a file does not exist, and
            \033[33m403\033[0m if a user tries to access a file they don't have access to

            you can load a plugin which will be invoked right before this
            happens, and the plugin can choose to override this behavior

            load the plugin using --args or volflags; for example \033[36m
             --on404 ~/partyhandlers/not404.py
             -v .::r:c,on404=~/partyhandlers/not404.py
            \033[0m
            the file must define the function \033[35mmain(cli,vn,rem)\033[0m:
             \033[35mcli\033[0m: the copyparty HttpCli instance
             \033[35mvn\033[0m:  the VFS which overlaps with the requested URL
             \033[35mrem\033[0m: the remainder of the URL below the VFS mountpoint

            `main` must return a string; one of the following:

            > \033[32m"true"\033[0m: the plugin has responded to the request,
                and the TCP connection should be kept open

            > \033[32m"false"\033[0m: the plugin has responded to the request,
                and the TCP connection should be terminated

            > \033[32m"retry"\033[0m: the plugin has done something to resolve the 404
                situation, and copyparty should reattempt reading the file.
                if it still fails, a regular 404 will be returned

            > \033[32m"allow"\033[0m: should ignore the insufficient permissions
                and let the client continue anyways

            > \033[32m""\033[0m: the plugin has not handled the request;
                try the next plugin or return the usual 404 or 403

            \033[1;35mPS!\033[0m the folder that contains the python file should ideally
              not contain many other python files, and especially nothing
              with filenames that overlap with modules used by copyparty
            """
            ),
        ],
        [
            "hooks",
            "execute commands before/after various events",
            dedent(
                """
            execute a command (a program or script) before or after various events;
             \033[36mxbu\033[35m executes CMD before a file upload starts
             \033[36mxau\033[35m executes CMD after  a file upload finishes
             \033[36mxiu\033[35m executes CMD after  all uploads finish and volume is idle
             \033[36mxbc\033[35m executes CMD before a file copy
             \033[36mxac\033[35m executes CMD after  a file copy
             \033[36mxbr\033[35m executes CMD before a file rename/move
             \033[36mxar\033[35m executes CMD after  a file rename/move
             \033[36mxbd\033[35m executes CMD before a file delete
             \033[36mxad\033[35m executes CMD after  a file delete
             \033[36mxm\033[35m executes CMD on message
             \033[36mxban\033[35m executes CMD if someone gets banned
            \033[0m
            can be defined as --args or volflags; for example \033[36m
             --xau foo.py
             -v .::r:c,xau=bar.py
            \033[0m
            hooks specified as commandline --args are appended to volflags;
            each commandline --arg and volflag can be specified multiple times,
            each hook will execute in order unless one returns non-zero, or
            "100" which means "stop daisychaining and return 0 (success/OK)"

            optionally prefix the command with comma-sep. flags similar to -mtp:

             \033[36mf\033[35m forks the process, doesn't wait for completion
             \033[36mc\033[35m checks return code, blocks the action if non-zero
             \033[36mj\033[35m provides json with info as 1st arg instead of filepath
             \033[36ms\033[35m provides input data on stdin (instead of 1st arg)
             \033[36mwN\033[35m waits N sec after command has been started before continuing
             \033[36mtN\033[35m sets an N sec timeout before the command is abandoned
             \033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
             \033[36mI\033[35m import and run as module, not as subprocess

             \033[36mar\033[35m only run hook if user has read-access
             \033[36marw\033[35m only run hook if user has read-write-access
             \033[36marwmd\033[35m ...and so on... (doesn't work for xiu or xban)

             \033[36mkt\033[35m kills the entire process tree on timeout (default),
             \033[36mkm\033[35m kills just the main process
             \033[36mkn\033[35m lets it continue running until copyparty is terminated

             \033[36mc0\033[35m show all process output (default)
             \033[36mc1\033[35m show only stderr
             \033[36mc2\033[35m show only stdout
             \033[36mc3\033[35m mute all process output
            \033[0m
            examples:

             \033[36m--xm some.py\033[35m runs \033[33msome.py msgtxt\033[35m on each 📟 message;
              \033[33mmsgtxt\033[35m is the message that was written into the web-ui

             \033[36m--xm j,some.py\033[35m runs \033[33msome.py jsontext\033[35m on each 📟 message;
              \033[33mjsontext\033[35m is the message info (ip, user, ..., msg-text)

             \033[36m--xm aw,j,some.py\033[35m requires user to have write-access

             \033[36m--xm aw,,notify-send,hey,--\033[35m shows an OS alert on linux;
              the \033[33m,,\033[35m stops copyparty from reading the rest as flags and
              the \033[33m--\033[35m stops notify-send from reading the message as args
              and the alert will be "hey" followed by the messagetext

             \033[36m--xm s,,tee,-a,log.txt\033[35m appends each msg to log.txt;
             \033[36m--xm s,j,,tee,-a,log.txt\033[35m writes it as json instead

             \033[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq;
             \033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can
             \033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example
            \033[0m
            each hook is executed once for each event, except for \033[36mxiu\033[0m
            which builds up a backlog of uploads, running the hook just once
            as soon as the volume has been idle for iN seconds (5 by default)

            \033[36mxiu\033[0m is also unique in that it will pass the metadata to the
            executed program on STDIN instead of as argv arguments (so
            just like the \033[36ms\033[0m option does for the other hook types), and
            it also includes the wark (file-id/hash) as a json property

            \033[36mxban\033[0m can be used to overrule / cancel a user ban event;
            if the program returns 0 (true/OK) then the ban will NOT happen

            effects can be used to redirect uploads into other
            locations, and to delete or index other files based
            on new uploads, but with certain limitations. See
            bin/hooks/reloc* and docs/devnotes.md#hook-effects

            the \033[36mI\033[0m option will override most other options, because
            it entirely hands over control to the hook, which is
            then able to tamper with copyparty's internal memory
            and wreck havoc if it wants to -- but this is worh it
            because it makes the hook 140x faster

            except for \033[36mxm\033[0m, only one hook / one action can run at a time,
            so it's recommended to use the \033[36mf\033[0m flag unless you really need
            to wait for the hook to finish before continuing (without \033[36mf\033[0m
            the upload speed can easily drop to 10% for small files)"""
            ),
        ],
        [
            "idp",
            "replacing the login system with fancy middleware",
            dedent(
                """
            if you already have a centralized service which handles
            user-authentication for other services already, you can
            integrate copyparty with that for automatic login

            if the middleware is providing the username in an http-header
            named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m

            if the middleware is providing a list of groups in the header
            named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m

            if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m

            if the middleware is providing a header named '\033[35mAccount\033[0m'
            and the value is '\033[35malice@forest.net\033[0m' but the username is
            actually '\033[35mmarisa\033[0m' then do this for each user:
            \033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
            (the separator '\033[35m^\033[0m' can be any character)

            make ABSOLUTELY SURE that the header can only be set by your
            middleware and not by clients! and, as an extra precaution,
            send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
            and then \033[36m--idp-h-key finalmasterspark\033[0m to require that

            the login/logout links/buttons can be replaced with links
            going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
            will expand \033[36m{dst}\033[0m to the URL of the current page, so
            the IdP can redirect the user back to where they were
            """
            ),
        ],
        [
            "urlform",
            "how to handle url-form POSTs",
            dedent(
                """
            values for --urlform:
              \033[36mstash\033[35m dumps the data to file and returns length + checksum
              \033[36msave,get\033[35m dumps to file and returns the page like a GET
              \033[36mget\033[35m ignores the message and returns the page like a GET
              \033[36mprint    \033[35m prints the data to log and returns an error
              \033[36mprint,xm \033[35m prints the data to log and returns --xm output
              \033[36mprint,get\033[35m prints the data to log and returns GET\033[0m

            note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m is
              either \033[36mprint\033[0m or \033[36mprint,get\033[0m or the default \033[36mprint,xm\033[0m

            if an \033[35m--xm\033[0m hook returns text, then
              the response code will be HTTP 202;
              http/get responses will be HTTP 200

            if there are multiple \033[35m--xm\033[0m hooks defined, then
              the first hook that produced output is returned

            if there are no \033[35m--xm\033[0m hooks defined, then the default
              \033[36mprint,xm\033[0m behaves like \033[36mprint,get\033[0m (returning html)
            """
            ),
        ],
        [
            "exp",
            "text expansion",
            dedent(
                """
            specify --exp or the "exp" volflag to enable placeholder expansions
            in README.md / PREADME.md / .prologue.html / .epilogue.html

            --exp-md (volflag exp_md) holds the list of placeholders which can be
            expanded in READMEs, and --exp-lg (volflag exp_lg) likewise for logues;
            any placeholder not given in those lists will be ignored and shown as-is

            the default list will expand the following placeholders:
            \033[36m{{self.ip}}     \033[35mclient ip
            \033[36m{{self.ua}}     \033[35mclient user-agent
            \033[36m{{self.uname}}  \033[35mclient username
            \033[36m{{self.host}}   \033[35mthe "Host" header, or the server's external IP otherwise
            \033[36m{{cfg.name}}    \033[35mthe --name global-config
            \033[36m{{cfg.logout}}  \033[35mthe --logout global-config
            \033[36m{{vf.scan}}     \033[35mthe "scan" volflag
            \033[36m{{vf.thsize}}   \033[35mthumbnail size
            \033[36m{{srv.itime}}   \033[35mserver time in seconds
            \033[36m{{srv.htime}}   \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)
            \033[36m{{hdr.cf-ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
            \033[0m
            so the following types of placeholders can be added to the lists:
            * any client header can be accessed through {{hdr.*}}
            * any variable in httpcli.py can be accessed through {{self.*}}
            * any global server setting can be accessed through {{cfg.*}}
            * any volflag can be accessed through {{vf.*}}

            remove vf.scan from default list using --exp-md /vf.scan
            add "accept" header to def. list using --exp-md +hdr.accept

            for performance reasons, expansion only happens while embedding
            documents into directory listings, and when accessing a ?doc=...
            link, but never otherwise, so if you click a -txt- link you'll
            have to refresh the page to apply expansion
            """
            ),
        ],
        [
            "rlo",
            "logrotate format",
            dedent(
                """
            a logrotate-counter is added if the logfile filename is taken;
            by default at the end, unless \033[32m%R\033[0m is somewhere in the \033[36m-lo\033[0m pattern,
            for example:  -lo /var/log/cpp/%Y-%m-%d%R.txt

            \033[36m--rlo\033[0m configures the logrotate format; examples:
            .1   = when necessary, append a dot followed by a single digit
            .1!  = counter is always added, even when not necessary
            -3   = a hyphen followed by three-digit counter
            (blank) = disable counter; overwrite existing logfile
            """
            ),
        ],
        [
            "ls",
            "volume inspection",
            dedent(
                """
            \033[35m--ls USR,VOL,FLAGS
              \033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users
              \033[36mVOL\033[0m is a single volume to scan, default is * (all vols)
              \033[36mFLAG\033[0m is flags;
                \033[36mv\033[0m in addition to realpaths, print usernames and vpaths
                \033[36mln\033[0m only prints symlinks leaving the volume mountpoint
                \033[36mp\033[0m exits 1 if any such symlinks are found
                \033[36mr\033[0m resumes startup after the listing

            examples:
              --ls '**'          # list all files which are possible to read
              --ls '**,*,ln'     # check for dangerous symlinks
              --ls '**,*,ln,p,r' # check, then start normally if safe
            """
            ),
        ],
        [
            "dbd",
            "database durability profiles",
            dedent(
                """
            mainly affects uploads of many small files on slow HDDs; speeds measured uploading 520 files on a WD20SPZX (SMR 2.5" 5400rpm 4kb)

            \033[32macid\033[0m = extremely safe but slow; the old default. Should never lose any data no matter what

            \033[32mswal\033[0m = 2.4x faster uploads yet 99.9% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes

            \033[32mwal\033[0m = another 21x faster on HDDs yet 90% as safe; same pitfall as \033[33mswal\033[0m except more likely

            \033[32myolo\033[0m = another 1.5x faster, and removes the occasional sudden upload-pause while the disk syncs, but now you're at risk of losing the entire database in a powerloss / OS-crash

            profiles can be set globally (--dbd=yolo), or per-volume with volflags: -v ~/Music:music:r:c,dbd=acid
            """
            ),
        ],
        [
            "chmod",
            "file/folder permissions",
            dedent(
                """
            global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file

            similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm

            the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc.

            if 3 digits: User, Group, Other
            if 4 digits: Special, User, Group, Other

            "Special" digit (sum of the following):
            \033[32m1\033[0m = sticky (files: n/a; dirs: files in directory can be deleted only by their owners)
            \033[32m2\033[0m = setgid (files: run executable as file group; dirs: files inherit group from directory)
            \033[32m4\033[0m = setuid (files: run executable as file owner; dirs: n/a)

            "User", "Group", "Other" digits for files:
            \033[32m0\033[0m = \033[35m---\033[0m = no access
            \033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
            \033[32m2\033[0m = \033[35m-w-\033[0m = can write
            \033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
            \033[32m4\033[0m = \033[35mr--\033[0m = can read
            \033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
            \033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
            \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute

            "User", "Group", "Other" digits for directories/folders:
            \033[32m0\033[0m = \033[35m---\033[0m = no access
            \033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
            \033[32m2\033[0m = \033[35m-w-\033[0m = n/a
            \033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
            \033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
            \033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
            \033[32m6\033[0m = \033[35mrw-\033[0m = n/a
            \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
            """
            ),
        ],
        [
            "pwhash",
            "password hashing",
            dedent(
                """
            when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed

            passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but
            copyparty will also hash and print any passwords that are non-hashed
            (password which do not start with '+') and then terminate afterwards

            if you have enabled --usernames then the password
            must be provided as username:password for hashing

            \033[36m--ah-alg\033[0m specifies the hashing algorithm and a
               list of optional comma-separated arguments:

            \033[36m--ah-alg argon2\033[0m  # which is the same as:
            \033[36m--ah-alg argon2,3,256,4,19\033[0m
            use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)

            \033[36m--ah-alg scrypt\033[0m  # which is the same as:
            \033[36m--ah-alg scrypt,13,2,8,4,32\033[0m
            use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads,
              and allow using up to 32 MiB RAM (ram=cost*blksz roughly)

            \033[36m--ah-alg sha2\033[0m  # which is the same as:
            \033[36m--ah-alg sha2,424242\033[0m
            use sha2-512 with 424242 iterations

            recommended: \033[32m--ah-alg argon2\033[0m
              (takes about 0.4 sec and 256M RAM to process a new password)

            argon2 needs python-package argon2-cffi,
            scrypt needs openssl,
            sha2 is always available
            """
            ),
        ],
        [
            "zm",
            "mDNS debugging",
            dedent(
                """
            the mDNS protocol is multicast-based, which means there are thousands
            of fun and interesting ways for it to break unexpectedly

            things to check if it does not work at all:

            * is there a firewall blocking port 5353 on either the server or client?
              (for example, clients may be able to send queries to copyparty,
               but the replies could get lost)

            * is multicast accidentally disabled on either the server or client?
              (look for mDNS log messages saying "new client on [...]")

            * the router/switch must be multicast and igmp capable

            things to check if it works for a while but then it doesn't:

            * is there a firewall blocking port 5353 on either the server or client?
              (copyparty may be unable to see the queries from the clients, but the
               clients may still be able to see the initial unsolicited announce,
               so it works for about 2 minutes after startup until TTL expires)

            * does the client have multiple IPs on its interface, and some of the
              IPs are in subnets which the copyparty server is not a member of?

            for both of the above intermittent issues, try --zm-spam 30
            (not spec-compliant but nothing will mind)
            """
            ),
        ],
    ]


def build_flags_desc():
    ret = ""
    for grp, flags in flagcats.items():
        ret += "\n\n\033[0m" + grp
        for k, v in flags.items():
            v = v.replace("\n", "\n    ")
            ret += "\n  \033[36m{}\033[35m {}".format(k, v)

    return ret


# fmt: off


def add_general(ap, nc, srvname):
    ap2 = ap.add_argument_group("general options")
    ap2.add_argument("-c", metavar="PATH", type=u, default=CFG_DEF, action="append", help="\033[34mREPEATABLE:\033[0m add config file")
    ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients")
    ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
    ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
    ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
    ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
    ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes")
    ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
    ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
    ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
    ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
    ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
    ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
    ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]")
    ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="expand environment-variables in config-files? [\033[32m0\033[0m]=no, [\033[32m1\033[0m]=$VAR (old scary syntax), [\033[32m2\033[0m]=${VAR} (new recommended syntax); default is new-syntax with panic if old-syntax is seen")
    ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
    ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
    ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
    ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best")
    ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)")
    ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks")
    ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable")
    ap2.add_argument("--license", action="store_true", help="show licenses and exit")
    ap2.add_argument("--version", action="store_true", help="show versions and exit")
    ap2.add_argument("--versionb", action="store_true", help="show version and exit")


def add_qr(ap, tty):
    ap2 = ap.add_argument_group("qr options")
    ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
    ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
    ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
    ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
    ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
    ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
    ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
    ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
    ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
    ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
    ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
    ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
    ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
    ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
    ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")


def add_fs(ap):
    ap2 = ap.add_argument_group("filesystem options")
    rm_re_def = "15/0.1" if ANYWIN else "0/0"
    ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)")
    ap2.add_argument("--fsnt", metavar="OS", type=u, default="auto", help="which characters to allow in file/folder names; [\033[32mwin\033[0m] = windows (not <>:|?*\"\\/), [\033[32mmac\033[0m] = macos (not :), [\033[32mlin\033[0m] = linux (anything goes) (volflag=fsnt)")
    ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
    ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
    ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
    ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")


def add_share(ap):
    db_path = os.path.join(E.cfg, "shares.db")
    ap2 = ap.add_argument_group("share-url options")
    ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
    ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
    ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
    ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
    ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
    ap2.add_argument("--shr-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating share-links; example: [\033[32mhttps://example.com/\033[0m]")
    ap2.add_argument("--shr-v", action="store_true", help="debug")


def add_upload(ap):
    ap2 = ap.add_argument_group("upload options")
    ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
    ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
    ap2.add_argument("--up-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating links to uploaded files; example: [\033[32mhttps://example.com/\033[0m]")
    ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)")
    ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
    ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
    ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
    ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
    ap2.add_argument("--apnd-who", metavar="NUM", type=u, default="dw", help="who can append to existing files? [\033[32mno\033[0m]=nobody, [\033[32maw\033[0m]=admin+write, [\033[32mdw\033[0m]=delete+write, [\033[32mw\033[0m]=write (volflag=apnd_who)")
    ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
    ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
    ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
    ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
    ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
    ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
    ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
    ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
    ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
    ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
    ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
    ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
    ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
    ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
    ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
    ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
    ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
    ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
    ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
    ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
    ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
    ap2.add_argument("--rm-partial", action="store_true", help="delete the .PARTIAL file when an unfinished upload expires after \033[33m--snap-drop\033[0m (volflag=rm_partial)")
    ap2.add_argument("--u2ts", metavar="TXT", type=u, default="c", help="how to timestamp uploaded files; [\033[32mc\033[0m]=client-last-modified, [\033[32mu\033[0m]=upload-time, [\033[32mfc\033[0m]=force-c, [\033[32mfu\033[0m]=force-u (volflag=u2ts)")
    ap2.add_argument("--rotf-tz", metavar="TXT", type=u, default="UTC", help="default timezone for the rotf upload rule; examples: [\033[32mEurope/Oslo\033[0m], [\033[32mAmerica/Toronto\033[0m], [\033[32mAntarctica/South_Pole\033[0m] (volflag=rotf_tz)")
    ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m chars long (volflag=rand)")
    ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
    ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
    ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
    ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
    ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
    ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
    ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
    ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
    ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
    ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
    ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")


def add_network(ap):
    ap2 = ap.add_argument_group("network options")
    ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (comma-separated list; see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6")
    ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets")
    ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
    ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=9999999, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m-1\033[0m]=closest-proxy, [\033[32m-2\033[0m]=second-hop, [\033[32m-3\033[0m]=third-hop")
    ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
    ap2.add_argument("--xf-host", metavar="NAME", type=u, default="x-forwarded-host", help="if reverse-proxied, which http header to read the correct Host value from; this header must contain the server's external domain name")
    ap2.add_argument("--xf-proto", metavar="NAME", type=u, default="x-forwarded-proto", help="if reverse-proxied, which http header to read the correct protocol value from; this header must contain either 'http' or 'https'")
    ap2.add_argument("--xf-proto-fb", metavar="T", type=u, default="", help="protocol to assume if the X-Forwarded-Proto header (\033[33m--xf-proto\033[0m) is not provided by the reverseproxy; either 'http' or 'https'")
    ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
    ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]\n └─for performance and security, this only looks at the TCP/Network-level IP, and will NOT work behind a reverseproxy")
    ap2.add_argument("--ipar", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated).\n └─this is reverseproxy-compatible; reads client-IP from 'X-Forwarded-For' if possible, with TCP/Network IP as fallback")
    ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
    ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)")
    ap2.add_argument("--http-vary", metavar="TXT", type=u, default="Origin, PW, Cookie", help="value of the 'Vary' response-header; a hint for caching proxies")
    ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
    if ANYWIN:
        ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
    elif not MACOS:
        ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
    ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
    ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
    ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
    ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
    ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
    ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
    ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds")
    ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds")
    ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
    ap2.add_argument("--list-nics", action="store_true", help="debug: list detected network adapters")
    ap2.add_argument("--list-ips", action="store_true", help="debug: list detected LAN IPs")


def add_tls(ap, cert_path):
    ap2 = ap.add_argument_group("SSL/TLS options")
    ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
    ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
    ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain (if \033[33m--certkey\033[0m is not set), or just the certificate chain (if \033[33m--certkey\033[0m is set)")
    ap2.add_argument("--certkey", metavar="PATH", type=u, default="", help="path to file containing just the certificate key; if this is set, then \033[33m--cert\033[0m should only contain the certificate chain")
    ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
    ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
    ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
    ap2.add_argument("--ssl-log", metavar="PATH", type=u, default="", help="log master secrets for later decryption in wireshark")


def add_cert(ap, cert_path):
    cert_dir = os.path.dirname(cert_path)
    ap2 = ap.add_argument_group("TLS certificate generator options")
    ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
    ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
    ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m")
    ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
    ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
    ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert")
    ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
    ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650.0, help="ca-certificate expiration time in days")
    ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365.0, help="server-cert expiration time in days")
    ap2.add_argument("--crt-cn", metavar="TXT", type=u, default="partyco", help="CA/server-cert common-name")
    ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name")
    ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name")
    ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72.0, help="backdate in hours")
    ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: \033[32mecdsa-256 rsa-4096 rsa-2048\033[0m")


def add_auth(ap):
    idp_db = os.path.join(E.cfg, "idp.db")
    ses_db = os.path.join(E.cfg, "sessions.db")
    ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
    ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
    ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
    ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
    ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
    ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
    ap2.add_argument("--idp-chsub", metavar="TXT", type=u, default="", help="characters to replace in usernames/groupnames; a list of pairs of characters separated by | so for example | _| will replace spaces with _ to make configuration easier, or |%%_|^_|@_| will replace %%/^/@ with _")
    ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)")
    ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
    ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
    ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
    ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
    ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
    ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
    ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
    ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
    ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
    ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
    ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
    ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
    ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
    ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
    ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
    ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
    ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
    ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)


def add_chpw(ap):
    db_path = os.path.join(E.cfg, "chpw.json")
    ap2 = ap.add_argument_group("user-changeable passwords options")
    ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords")
    ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m do not allow password-changes for this comma-separated list of usernames")
    ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)")
    ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length")
    ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users")


def add_zeroconf(ap):
    ap2 = ap.add_argument_group("Zeroconf options")
    ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)")
    ap2.add_argument("--z-on", metavar="NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes\n └─example: \033[32meth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\033[0m")
    ap2.add_argument("--z-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes")
    ap2.add_argument("--z-chk", metavar="SEC", type=int, default=10, help="check for network changes every \033[33mSEC\033[0m seconds (0=disable)")
    ap2.add_argument("-zv", action="store_true", help="verbose all zeroconf backends")
    ap2.add_argument("--mc-hop", metavar="SEC", type=int, default=0, help="rejoin multicast groups every \033[33mSEC\033[0m seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [\033[32m300\033[0m] or [\033[32m180\033[0m]\n └─note: can be due to firewalls; make sure UDP port 5353 is open in both directions (on clients too)")


def add_zc_mdns(ap):
    ap2 = ap.add_argument_group("Zeroconf-mDNS options; also see --help-zm")
    ap2.add_argument("--zm", action="store_true", help="announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...")
    ap2.add_argument("--zm-on", metavar="NETS", type=u, default="", help="enable mDNS ONLY on the comma-separated list of subnets and/or interface names/indexes")
    ap2.add_argument("--zm-off", metavar="NETS", type=u, default="", help="disable mDNS on the comma-separated list of subnets and/or interface names/indexes")
    ap2.add_argument("--zm4", action="store_true", help="IPv4 only -- try this if some clients can't connect")
    ap2.add_argument("--zm6", action="store_true", help="IPv6 only")
    ap2.add_argument("--zmv", action="store_true", help="verbose mdns")
    ap2.add_argument("--zmvv", action="store_true", help="verboser mdns")
    ap2.add_argument("--zm-http", metavar="PORT", type=int, default=-1, help="port to announce for http/webdav; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
    ap2.add_argument("--zm-https", metavar="PORT", type=int, default=-1, help="port to announce for https/webdavs; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
    ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)")
    ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)")
    ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set, \033[32mDd\033[0m otherwise)")
    ap2.add_argument("--zm-ld", metavar="PATH", type=u, default="", help="link a specific folder for webdav shares")
    ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
    ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
    ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
    ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
    ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
    ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
    ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
    ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0.0, help="send unsolicited announce every \033[33mSEC\033[0m; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues")


def add_zc_ssdp(ap):
    ap2 = ap.add_argument_group("Zeroconf-SSDP options")
    ap2.add_argument("--zs", action="store_true", help="announce the enabled protocols over SSDP -- compatible with Windows")
    ap2.add_argument("--zs-on", metavar="NETS", type=u, default="", help="enable SSDP ONLY on the comma-separated list of subnets and/or interface names/indexes")
    ap2.add_argument("--zs-off", metavar="NETS", type=u, default="", help="disable SSDP on the comma-separated list of subnets and/or interface names/indexes")
    ap2.add_argument("--zsv", action="store_true", help="verbose SSDP")
    ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)")
    ap2.add_argument("--zsid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce")


def add_sftp(ap):
    ap2 = ap.add_argument_group("SFTP options")
    ap2.add_argument("--sftp", metavar="PORT", type=int, default=0, help="enable SFTP server on \033[33mPORT\033[0m, for example \033[32m3922")
    ap2.add_argument("--sftpv", action="store_true", help="verbose")
    ap2.add_argument("--sftpvv", action="store_true", help="verboser")
    ap2.add_argument("--sftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol")
    ap2.add_argument("--sftp4", action="store_true", help="only listen on IPv4")
    ap2.add_argument("--sftp-key", metavar="U K", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add ssh-key \033[33mK\033[0m for user \033[33mU\033[0m (username, space, key-type, space, base64); if user has multiple keys, then repeat this option for each key\n └─commandline example: --sftp-key 'david ssh-ed25519 AAAAC3NzaC...'\n └─config-file example: sftp-key: david ssh-ed25519 AAAAC3NzaC...")
    ap2.add_argument("--sftp-key2u", action="append", help=argparse.SUPPRESS)
    ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
    ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
    ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
    ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
    ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")


def add_ftp(ap):
    ap2 = ap.add_argument_group("FTP options (TCP only)")
    ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
    ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990")
    ap2.add_argument("--ftpv", action="store_true", help="verbose")
    ap2.add_argument("--ftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol")
    ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
    ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
    ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite")
    ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
    ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
    ap2.add_argument("--ftp-pr", metavar="P-P", type=u, default="", help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")


def add_webdav(ap):
    ap2 = ap.add_argument_group("WebDAV options")
    ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. Some webdav clients need this option for editing existing files; not necessary for clients that send the 'x-oc-mtime' header. Regardless, the delete-permission must always be given. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
    ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this")
    ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
    ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
    ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
    ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of user-agents which ARE webdav-clients, and expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank")
    ap2.add_argument("--dav-port", metavar="P", type=int, default=0, help="additional port to listen on for misbehaving webdav clients which pretend they are graphical browsers; an alternative/supplement to dav-ua1")
    ap2.add_argument("--ua-nodav", metavar="PTN", type=u, default=r"^(Mozilla/|NetworkingExtension/|com\.apple\.WebKit)", help="regex of user-agents which are NOT webdav-clients")
    ap2.add_argument("--p-nodav", metavar="P,P", type=u, default="", help="server-ports (comma-sep.) which are NOT webdav-clients; an alternative/supplement to ua-nodav")


def add_tftp(ap):
    ap2 = ap.add_argument_group("TFTP options (UDP only)")
    ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
    ap2.add_argument("--tftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol")
    ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4")
    ap2.add_argument("--tftpv", action="store_true", help="verbose")
    ap2.add_argument("--tftpvv", action="store_true", help="verboser")
    ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations")
    ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...")
    ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing")
    ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
    ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000")


def add_smb(ap):
    ap2 = ap.add_argument_group("SMB/CIFS options")
    ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!")
    ap2.add_argument("--smb-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol")
    ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)")
    ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
    ap2.add_argument("--smb-port", metavar="PORT", type=int, default=445, help="port to listen on -- if you change this value, you must NAT from TCP:445 to this port using iptables or similar")
    ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance")
    ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs")
    ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)")
    ap2.add_argument("--smb6", action="store_true", help="enable IPv6")
    ap2.add_argument("--smbv", action="store_true", help="verbose")
    ap2.add_argument("--smbvv", action="store_true", help="verboser")
    ap2.add_argument("--smbvvv", action="store_true", help="verbosest")


def add_opds(ap):
    ap2 = ap.add_argument_group("OPDS options")
    ap2.add_argument("--opds", action="store_true", help="enable opds -- allows e-book readers to browse and download files (volflag=opds)")
    ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")


def add_handlers(ap):
    ap2 = ap.add_argument_group("handlers (see --help-handlers)")
    ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
    ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 403s by executing \033[33mPY\033[0m file")
    ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff")


def add_hooks(ap):
    ap2 = ap.add_argument_group("event hooks (see --help-hooks)")
    ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file upload starts")
    ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after  a file upload finishes")
    ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after  all uploads finish and volume is idle")
    ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file copy")
    ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after  a file copy")
    ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file move/rename")
    ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after  a file move/rename")
    ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file delete")
    ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after  a file delete")
    ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m on message")
    ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)")
    ap2.add_argument("--hook-v", action="store_true", help="verbose hooks")


def add_stats(ap):
    ap2 = ap.add_argument_group("grafana/prometheus metrics endpoint")
    ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts")
    ap2.add_argument("--stats-u", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to access /.cpr/metrics even if they aren't admin")
    ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)")
    ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)")
    ap2.add_argument("--nos-vst", action="store_true", help="disable volume state metrics (indexing, analyzing, activity)")
    ap2.add_argument("--nos-dup", action="store_true", help="disable dupe-files metrics (good idea; very slow)")
    ap2.add_argument("--nos-unf", action="store_true", help="disable unfinished-uploads metrics")


def add_yolo(ap):
    ap2 = ap.add_argument_group("yolo options")
    ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
    ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
    ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
    ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection")
    ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
    ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")


def add_optouts(ap):
    ap2 = ap.add_argument_group("opt-outs")
    ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
    ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)")
    ap2.add_argument("--no-dav", action="store_true", help="disable webdav support")
    ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
    ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
    ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
    ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
    ap2.add_argument("-nih", action="store_true", help="no info hostname -- removes it from the UI corner, but the value of \033[33m--bname\033[0m still shows in the browsertab title")
    ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as \033[33m--du-who no\033[0m")
    ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
    ap2.add_argument("--smsg", metavar="T,T", type=u, default="POST", help="HTTP-methods to allow ?smsg for; will execute xm hooks like urlform / message-to-serverlog; dangerous example: [\033[32mGET,POST\033[0m]. \033[1;31mWARNING:\033[0m The default (POST) is safe, but GET is dangerous; security/CSRF hazard")
    ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
    ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
    ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
    ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
    ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
    ap2.add_argument("--ua-nozip", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from download-as-zip/tar; disable with [\033[32mno\033[0m] or blank")
    ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
    ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
    ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
    ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
    ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
    ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
    ap2.add_argument("--no-zls", action="store_true", help="disable browsing the contents of zip/cbz files, does not affect thumbnails")


def add_safety(ap):
    ap2 = ap.add_argument_group("safety options")
    ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
    ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --no-html --no-readme --no-logues --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih")
    ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
    ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
    ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
    ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
    ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)")
    ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)")
    ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
    ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile")
    ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
    ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings")
    ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites (volflag=noscript)")
    ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables --no-script (volflag=nohtml)")
    ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV")
    ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
    ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
    ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
    ap2.add_argument("--dont-ban", metavar="TXT", type=u, default="no", help="anyone at this accesslevel or above will not get banned: [\033[32mav\033[0m]=admin-in-volume, [\033[32maa\033[0m]=has-admin-anywhere, [\033[32mrw\033[0m]=read-write, [\033[32mauth\033[0m]=authenticated, [\033[32many\033[0m]=disable-all-bans, [\033[32mno\033[0m]=anyone-can-get-banned")
    ap2.add_argument("--banmsg", metavar="TXT", type=u, default="thank you for playing \u00a0 (see fileserver log and readme)", help="the response to send to banned users; can be @ban.html to send the contents of ban.html")
    ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
    ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
    ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
    ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
    ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
    ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to permissions g/G/h (decent replacement for \033[33m--ban-404\033[0m if that can't be used)")
    ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
    ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\..{3}|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 403/404-bans; disable with blank or [\033[32mno\033[0m]")
    ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
    ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
    ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
    ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
    ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
    ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
    ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")


def add_salt(ap, fk_salt, dk_salt, ah_salt):
    ap2 = ap.add_argument_group("salting options")
    ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
    ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
    ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
    ap2.add_argument("--ah-cli", action="store_true", help="launch an interactive shell which hashes passwords without ever storing or displaying the original passwords")
    ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
    ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
    ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
    ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
    ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
    ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")


def add_shutdown(ap):
    ap2 = ap.add_argument_group("shutdown options")
    ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
    ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
    ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing, [\033[32mthgen\033[0m] thumbnail-pregen")


def add_logging(ap):
    ap2 = ap.add_argument_group("logging options")
    ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
    ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
    ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color")
    ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
    ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
    ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
    ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
    ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
    ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
    ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
    ap2.add_argument("--log-date", metavar="TXT", type=u, default="", help="date-format, for example [\033[32m%%Y-%%m-%%d\033[0m] (default is disabled; no date, just HH:MM:SS)")
    ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
    ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
    ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
    ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
    ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
    ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
    ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[xwjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
    ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message")
    ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message")
    ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: <name>' when a file larger than \033[33mMiB\033[0m is about to be hashed")


def add_admin(ap):
    ap2 = ap.add_argument_group("admin panel options")
    ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
    ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
    ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks); same as --stack-who=no")
    ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)")
    ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
    ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
    ap2.add_argument("--ups-who", metavar="LVL", type=int, default=2, help="who can see recent uploads on the ?ru page? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone (volflag=ups_who)")
    ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins")
    ap2.add_argument("--stack-who", metavar="LVL", type=u, default="a", help="who can see the ?stack page (list of threads)? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admins, [\033[32mrw\033[0m]=read+write, [\033[32mall\033[0m]=everyone")
    ap2.add_argument("--stack-v", action="store_true", help="verbose ?stack")


def add_thumbnail(ap):
    th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
    th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10
    ap2 = ap.add_argument_group("thumbnail options")
    ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
    ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
    ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
    ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
    ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
    ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
    ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
    ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
    ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
    ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
    ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
    ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)")
    ap2.add_argument("--th-dec", metavar="LIBS", default="vips,raw,pil,ff", help="image decoders, in order of preference")
    ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
    ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
    ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output")
    ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
    ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)")
    ap2.add_argument("--th-vips-jxl", metavar="N", type=int, default=1, help="when to allow generating jxl thumbnails with libvips; 0=never, 1=musl-mallocng, 2=always")
    ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds")
    ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
    ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
    ap2.add_argument("--th-pregen", metavar="F,F", type=u, default="", help="pregenerate thumbnails on startup; \033[33mF,F\033[0m is comma-separated list of formats; example: [\033[32mj,jf,w,w3,wf,wf3,x,xf\033[0m] NOTE: remember to set \033[33m--th-maxage 123456789\033[0m (volflag=th_pregen)")
    ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit")
    ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
    ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
    # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
    # https://github.com/libvips/libvips
    # https://stackoverflow.com/a/47612661
    # ffmpeg -hide_banner -demuxers | awk '/^ D  /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
    ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
    ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,mdc,mef,mrw,nef,nii,pfm,pgm,png,ppm,raf,raw,sr2,srf,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
    ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy")
    ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
    ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
    ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
    ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
    ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")


def add_transcoding(ap):
    ap2 = ap.add_argument_group("transcoding options")
    ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable")
    ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable")
    ap2.add_argument("--allow-wav", action="store_true", help="allow transcoding to wav (lossless, uncompressed)")
    ap2.add_argument("--allow-flac", action="store_true", help="allow transcoding to flac (lossless, compressed)")
    ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead")
    ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead")
    ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
    ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)")
    ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")


def add_tail(ap):
    ap2 = ap.add_argument_group("tailing options (realtime streaming of a growing file)")
    ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)")
    ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
    ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
    ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
    ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
    ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")


def add_rss(ap):
    ap2 = ap.add_argument_group("RSS options")
    ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
    ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')")
    ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all")
    ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files (volflag=rss_sort)")
    ap2.add_argument("--rss-fmt-t", metavar="TXT", type=u, default="{fname}", help="title format (url-param 'rss_fmt_t') (volflag=rss_fmt_t)")
    ap2.add_argument("--rss-fmt-d", metavar="TXT", type=u, default="{artist} - {title}", help="description format (url-param 'rss_fmt_d') (volflag=rss_fmt_d)")


def add_db_general(ap, hcores):
    noidx = APPLESAN_TXT if MACOS else ""
    ap2 = ap.add_argument_group("general db options")
    ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication")
    ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m")
    ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m")
    ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
    ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
    ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
    ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
    ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
    ap2.add_argument("--fika", metavar="TXT", type=u, default="ucd", help="list of user-actions to allow while filesystem-indexer is still busy; set blank to never interrupt indexing (old default). [\033[32mu\033[0m]=uploads, [\033[32mc\033[0m]=filecopy, [\033[32mm\033[0m]=move/rename, [\033[32md\033[0m]=delete. NOTE: [\033[32mm\033[0m] is untested/scary, and blank is recommended if dedup enabled")
    ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
    ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
    ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
    ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
    ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
    ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
    ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
    ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
    ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
    ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
    ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
    ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
    ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
    ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
    ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
    ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
    ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
    ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)")


def add_db_metadata(ap):
    ap2 = ap.add_argument_group("metadata db options")
    ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
    ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m")
    ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m")
    ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will detect more tags")
    ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer")
    ap2.add_argument("--mtag-to", metavar="SEC", type=int, default=60, help="timeout for FFprobe tag-scan")
    ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning")
    ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such")
    ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers")
    ap2.add_argument("--db-xattr", metavar="t,t", type=u, default="", help="read file xattrs as metadata tags; [\033[32ma,b\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33ma\033[0m and \033[33mb\033[0m, [\033[32ma=foo,b=bar\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33mfoo\033[0m and \033[33mbar\033[0m, [\033[32m~~a,b\033[0m] does everything except \033[33ma\033[0m and \033[33mb\033[0m, [\033[32m~~\033[0m] does everything. NOTE: Each tag must also be enabled with \033[33m-mte\033[0m (volflag=db_xattr)")
    ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add/replace metadata mapping")
    ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE)
    ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH)
    ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file")
    ap2.add_argument("--have-db-xattr", action="store_true", help=argparse.SUPPRESS)


def add_txt(ap):
    ap2 = ap.add_argument_group("textfile options")
    ap2.add_argument("--rw-edit", metavar="T,T", type=u, default="md", help="comma-sep. list of file-extensions to allow editing with permissions read+write; all others require read+write+delete (volflag=rw_edit)")
    ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
    ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
    ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
    ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
    ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
    ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)")
    ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
    ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
    ap2.add_argument("--ua-nodoc", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [\033[32mno\033[0m] or blank")


def add_og(ap):
    ap2 = ap.add_argument_group("og / open graph / discord-embed options")
    ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
    ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
    ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
    ap2.add_argument("--og-no-head", action="store_true", help="do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such) (volflag=og_no_head)")
    ap2.add_argument("--og-th", metavar="FMT", type=u, default="jf3", help="thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ... (volflag=og_th)")
    ap2.add_argument("--og-title", metavar="TXT", type=u, default="", help="fallback title if there is nothing in the \033[33m-e2t\033[0m database (volflag=og_title)")
    ap2.add_argument("--og-title-a", metavar="T", type=u, default="🎵 {{ artist }} - {{ title }}", help="audio title format; takes any metadata key (volflag=og_title_a)")
    ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)")
    ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)")
    ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)")
    ap2.add_argument("--og-desc", metavar="TXT", type=u, default="", help="description text; same for all files, disable with [\033[32m-\033[0m] (volflag=og_desc)")
    ap2.add_argument("--og-site", metavar="TXT", type=u, default="", help="sitename; defaults to \033[33m--name\033[0m, disable with [\033[32m-\033[0m] (volflag=og_site)")
    ap2.add_argument("--tcolor", metavar="RGB", type=u, default="333", help="accent color (3 or 6 hex digits); may also affect safari and/or android-chrome (volflag=tcolor)")
    ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")


def add_ui(ap, retry ):
    THEMES = 10
    ap2 = ap.add_argument_group("ui options")
    ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
    ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
    ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
    ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, 6, 6c, 7, 7c, fuzzy (see UI)")
    ap2.add_argument("--gauto", metavar="PERCENT", type=int, default=0, help="switch to gridview if more than \033[33mPERCENT\033[0m of files are pics/vids; 0=disabled")
    ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double")
    ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
    ap2.add_argument("--glang", action="store_true", help="guess the browser's default language, otherwise fall back to \033[33m--lang\033[0m")
    ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
    ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
    ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
    ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
    ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
    ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
    ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
    ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
    ap2.add_argument("--dlni", action="store_true", help="force download (don't show inline) when files are clicked (volflag:dlni)")
    ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
    ap2.add_argument("--dothidden", action="store_true", help="hide specific files in a folder by listing them in a file named .hidden -- WARNING: Mostly cosmetic! Download-as-zip/tar will still download them. Do not rely on this for security (volflag=dothidden)")
    ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
    ap2.add_argument("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)")
    ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
    ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
    ap2.add_argument("--notooltips", action="store_true", help="tooltips disabled as default")
    ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
    ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html")
    ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
    ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
    ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
    ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
    ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
    ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
    ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
    ap2.add_argument("--prologues", metavar="T,T", type=u, default=".prologue.html", help="comma-sep. list of filenames to scan for and use as prologues (embed above/before directory listing) (volflag=prologues)")
    ap2.add_argument("--epilogues", metavar="T,T", type=u, default=".epilogue.html", help="comma-sep. list of filenames to scan for and use as epilogues (embed below/after directory listing) (volflag=epilogues)")
    ap2.add_argument("--preadmes", metavar="T,T", type=u, default="preadme.md,PREADME.md", help="comma-sep. list of filenames to scan for and use as preadmes (embed above/before directory listing) (volflag=preadmes)")
    ap2.add_argument("--readmes", metavar="T,T", type=u, default="readme.md,README.md", help="comma-sep. list of filenames to scan for and use as readmes (embed below/after directory listing) (volflag=readmes)")
    ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
    ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
    ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
    ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
    ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
    ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
    ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
    ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
    ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
    ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
    ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
    ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox")
    ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)")
    ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)")
    ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
    ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
    ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
    ap2.add_argument("--ui-nombar", action="store_true", help="hide top-menu in the UI (volflag=ui_nombar)")
    ap2.add_argument("--ui-noacci", action="store_true", help="hide account-info in the UI (volflag=ui_noacci)")
    ap2.add_argument("--ui-nosrvi", action="store_true", help="hide server-info in the UI (volflag=ui_nosrvi)")
    ap2.add_argument("--ui-nonav", action="store_true", help="hide navpane+breadcrumbs (volflag=ui_nonav)")
    ap2.add_argument("--ui-notree", action="store_true", help="hide navpane in the UI (volflag=ui_notree)")
    ap2.add_argument("--ui-nocpla", action="store_true", help="hide cpanel-link in the UI (volflag=ui_nocpla)")
    ap2.add_argument("--ui-nolbar", action="store_true", help="hide link-bar in the UI (volflag=ui_nolbar)")
    ap2.add_argument("--ui-noctxb", action="store_true", help="hide context-buttons in the UI (volflag=ui_noctxb)")
    ap2.add_argument("--ui-norepl", action="store_true", help="hide repl-button in the UI (volflag=ui_norepl)")
    ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS)


def add_debug(ap):
    ap2 = ap.add_argument_group("debug options")
    ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
    ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
    ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies")
    if hasattr(select, "poll"):
        ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)")
    ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
    ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
    ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
    ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
    ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
    ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
    ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
    ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
    ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m")
    ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
    ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")
    ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than \033[33mNUM\033[0m files at \033[33m--kf-dir\033[0m already; default: 6.3 GiB max (200*32M)")
    ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at \033[33mPATH\033[0m; default: folder named 'bf' wherever copyparty was started")
    ap2.add_argument("--bf-log", metavar="PATH", type=u, default="", help="bak-flips: log corruption info to a textfile at \033[33mPATH\033[0m")
    ap2.add_argument("--no-cfg-cmt-warn", action="store_true", help=argparse.SUPPRESS)


# fmt: on


def run_argparse(
    argv , formatter , retry , nc , verbose=True
)  :
    ap = argparse.ArgumentParser(
        formatter_class=formatter,
        usage=argparse.SUPPRESS,
        prog="copyparty",
        description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
    )

    cert_path = os.path.join(E.cfg, "cert.pem")

    fk_salt = get_salt("fk", 18)
    dk_salt = get_salt("dk", 30)
    ah_salt = get_salt("ah", 18)

    # alpine peaks at 5 threads for some reason,
    # all others scale past that (but try to avoid SMT),
    # 5 should be plenty anyways (3 GiB/s on most machines)
    hcores = min(CORES, 5 if CORES > 8 else 4)

    tty = os.environ.get("TERM", "").lower() == "linux"

    srvname = get_srvname(verbose)

    add_general(ap, nc, srvname)
    add_network(ap)
    add_tls(ap, cert_path)
    add_cert(ap, cert_path)
    add_auth(ap)
    add_chpw(ap)
    add_qr(ap, tty)
    add_zeroconf(ap)
    add_zc_mdns(ap)
    add_zc_ssdp(ap)
    add_fs(ap)
    add_share(ap)
    add_upload(ap)
    add_db_general(ap, hcores)
    add_db_metadata(ap)
    add_thumbnail(ap)
    add_transcoding(ap)
    add_rss(ap)
    add_sftp(ap)
    add_ftp(ap)
    add_webdav(ap)
    add_tftp(ap)
    add_smb(ap)
    add_opds(ap)
    add_safety(ap)
    add_salt(ap, fk_salt, dk_salt, ah_salt)
    add_optouts(ap)
    add_shutdown(ap)
    add_yolo(ap)
    add_handlers(ap)
    add_hooks(ap)
    add_stats(ap)
    add_txt(ap)
    add_tail(ap)
    add_og(ap)
    add_ui(ap, retry)
    add_admin(ap)
    add_logging(ap)
    add_debug(ap)

    ap2 = ap.add_argument_group("help sections")
    sects = get_sects()
    for k, h, _ in sects:
        ap2.add_argument("--help-" + k, action="store_true", help=h)

    if retry:
        a = ["ascii", "replace"]
        for x in ap._actions:
            try:
                x.default = x.default.encode(*a).decode(*a)
            except:
                pass

            try:
                if x.help and x.help is not argparse.SUPPRESS:
                    x.help = x.help.replace("└─", "`-").encode(*a).decode(*a)
                    if retry > 2:
                        x.help = RE_ANSI.sub("", x.help)
            except:
                pass

    ret = ap.parse_args(args=argv[1:])
    for k, h, t in sects:
        k2 = "help_" + k.replace("-", "_")
        if vars(ret)[k2]:
            lprint("# %s help page (%s)" % (k, h))
            lprint(t.rstrip() + "\033[0m")
            sys.exit(0)

    return ret


def main(argv  = None)  :
    if argv is None:
        argv = sys.argv

    if "--versionb" in argv:
        print(S_VERSION)
        sys.exit(0)

    time.strptime("19970815", "%Y%m%d")  # python#7980
    if WINDOWS:
        os.system("rem")  # enables colors

    init_E(E)

    f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n   sqlite {} | jinja {} | pyftpd {} | tftp {} | miko {}\n\033[0m'
    f = f.format(
        S_VERSION,
        CODENAME,
        S_BUILD_DT,
        PY_DESC.replace("[", "\033[90m["),
        SQLITE_VER,
        JINJA_VER,
        PYFTPD_VER,
        PARTFTPY_VER,
        MIKO_VER,
    )
    lprint(f)

    if "--version" in argv:
        sys.exit(0)

    if "--license" in argv:
        showlic()
        sys.exit(0)

    if "--mimes" in argv:
        print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
        sys.exit(0)

    if "--list-ips" in argv:
        print("\n".join(str(x) for x in sorted(list_ips())))
        sys.exit(0)

    if "--list-nics" in argv:
        print("\n".join(str(x) for x in sorted(list_nics(True).items())))
        sys.exit(0)

    if EXE:
        print("pybin: {}\n".format(pybin), end="")

    for n, zs in enumerate(argv):
        if zs.startswith("--sfx-tpoke="):
            Daemon(sfx_tpoke, "sfx-tpoke", (zs.split("=", 1)[1],))
            argv.pop(n)
            break

    ensure_locale()

    ensure_webdeps()

    argv = expand_cfg(argv)

    deprecated   = [
        ("--salt", "--warksalt"),
        ("--hdr-au-usr", "--idp-h-usr"),
        ("--idp-h-sep", "--idp-gsep"),
        ("--th-no-crop", "--th-crop=n"),
        ("--never-symlink", "--hardlink-only"),
    ]
    for dk, nk in deprecated:
        idx = -1
        ov = ""
        for n, k in enumerate(argv):
            if k == dk or k.startswith(dk + "="):
                idx = n
                if "=" in k:
                    ov = "=" + k.split("=", 1)[1]

        if idx < 0:
            continue

        msg = "\033[1;31mWARNING:\033[0;1m\n  {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
        lprint(msg.format(dk, nk))
        argv[idx] = nk + ov
        time.sleep(2)

    da = len(argv) == 1 and not CFG_DEF
    try:
        if da:
            argv.extend(["--qr"])
            if ANYWIN or not os.geteuid():
                # win10 allows symlinks if admin; can be unexpected
                argv.extend(["-p80,443,3923", "--ign-ebind"])
    except:
        pass

    if da:
        t = "no arguments provided; will use {}\n"
        lprint(t.format(" ".join(argv[1:])))

    nc = 1024
    try:
        import resource

        _, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
        if hard > 0:  # -1 == infinite
            nc = min(nc, int(hard / 4))
    except:
        nc = 486  # mdns/ssdp restart headroom; select() maxfd is 512 on windows

    retry = 0
    for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
        try:
            al = run_argparse(argv, fmtr, retry, nc)
            dal = run_argparse([], fmtr, retry, nc, False)
            break
        except SystemExit:
            raise
        except:
            retry += 1
            t = "WARNING: due to limitations in your terminal and/or OS, the helptext cannot be displayed correctly. Will show a simplified version due to the following error:\n[ %s ]:\n%s\n"
            lprint(t % (fmtr, min_ex()))

    try:
        assert al  # type: ignore
        assert dal  # type: ignore
        al.E = E  # __init__ is not shared when oxidized
    except:
        sys.exit(1)

    quotecheck(al)

    if al.env_expand == 2:
        al.shenvexp = expand_osenv_c
    elif al.env_expand == 1:
        al.shenvexp = expand_osenv_s
    elif al.env_expand == 0:
        al.shenvexp = expand_osenv_noop
    else:
        al.shenvexp = expand_osenv_cs

    if al.chdir:
        os.chdir(al.chdir)

    if al.ansi:
        al.no_ansi = False
    elif not al.no_ansi:
        al.ansi = VT100

    if WINDOWS and not al.keep_qem and not al.ah_cli:
        try:
            disable_quickedit()
        except:
            lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n")

    if not al.ansi:
        al.wintitle = ""

    # propagate implications
    for k1, k2 in IMPLICATIONS:
        if getattr(al, k1, None):
            setattr(al, k2, True)

    # propagate unplications
    for k1, k2 in UNPLICATIONS:
        if getattr(al, k1):
            setattr(al, k2, False)

    protos = "ftp tftp sftp smb".split()
    opts = ["%s_i" % (zs,) for zs in protos]
    for opt in opts:
        if getattr(al, opt) == "-i":
            setattr(al, opt, al.i)
    for opt in ["i"] + opts:
        zs = getattr(al, opt)
        if not HAVE_IPV6 and zs == "::":
            zs = "0.0.0.0"
        setattr(al, opt, [x.strip() for x in zs.split(",")])

    try:
        if "-" in al.p:
            lo, hi = [int(x) for x in al.p.split("-")]
            al.p = list(range(lo, hi + 1))
        else:
            al.p = [int(x) for x in al.p.split(",")]
    except:
        raise Exception("invalid value for -p")

    for arg, kname, okays in [["--u2sort", "u2sort", "s n fs fn"]]:
        val = unicode(getattr(al, kname))
        if val not in okays.split():
            zs = "argument {} cannot be '{}'; try one of these: {}"
            raise Exception(zs.format(arg, val, okays))

    if not al.qrs and [k for k in argv if k.startswith("--qr")]:
        al.qr = True

    if al.ihead:
        al.ihead = [x.lower() for x in al.ihead]

    if al.ohead:
        al.ohead = [x.lower() for x in al.ohead]

    if HAVE_SSL:
        if al.ssl_ver:
            configure_ssl_ver(al)

        if al.ciphers:
            configure_ssl_ciphers(al)
    else:
        warn("ssl module does not exist; cannot enable https")
        al.http_only = True

    if PY2 and WINDOWS and al.e2d:
        warn(
            "windows py2 cannot do unicode filenames with -e2d\n"
            + "  (if you crash with codec errors then that is why)"
        )

    if PY2 and al.smb:
        print("error: python2 cannot --smb")
        return

    if not PY36:
        al.no_scandir = True

    if not hasattr(os, "sendfile"):
        al.no_sendfile = True

    if not hasattr(select, "poll"):
        al.no_poll = True

    # signal.signal(signal.SIGINT, sighandler)

    SvcHub(al, dal, argv).run()


if __name__ == "__main__":
    main()
