From 41ed559faabdc180efc37fd027e7f1bb2d14d174 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 4 Oct 2025 13:32:26 +0000 Subject: [PATCH] hooks: import-flag --- bin/hooks/import-me.py | 54 ++++++++++++++++++++++++++++++++++++++++++ copyparty/__main__.py | 7 ++++++ copyparty/util.py | 14 +++++++---- docs/devnotes.md | 8 +++++++ 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 bin/hooks/import-me.py diff --git a/bin/hooks/import-me.py b/bin/hooks/import-me.py new file mode 100644 index 00000000..547f4172 --- /dev/null +++ b/bin/hooks/import-me.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +from typing import Any + +_ = r""" +the fastest hook in the west +(runs directly inside copyparty, not as a subprocess) + +example usage as global config: + --xbu I,bin/hooks/import-me.py + +example usage as a volflag (per-volume config): + -v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (share filesystem-path srv/inc as volume /inc, + readable by everyone, read-write for user 'ed', + running this plugin on all uploads with the params listed below) + +example usage as a volflag in a copyparty config file: + [/inc] + srv/inc + accs: + r: * + rw: ed + flags: + xbu: I,bin/hooks/import-me.py + +parameters explained, + I = import; do not fork / subprocess + +IMPORTANT NOTE: + because this hook is running inside copyparty, you need to + be EXCEPTIONALLY CAREFUL to avoid side-effects, for example + DO NOT os.chdir() or anything like that, and also make sure + that the name of this file is unique (cannot be the same as + an existing python module/library) +""" + + +def main(ka: dict[str, Any]) -> dict[str, str]: + # "ka" is a dictionary with info from copyparty... + + # but because we are running inside copyparty, we don't need such courtesies; + import inspect + + cf = inspect.currentframe().f_back.f_back.f_back + t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n" + t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9]) + print(t % (cf.f_code, t2)) + + # must return a dictionary with: + # "rc": the retcode; 0 is ok + return {"rc": 0} diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 2fae2df7..e88cdf8f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -816,6 +816,7 @@ def get_sects(): \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 @@ -865,6 +866,12 @@ def get_sects(): 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 diff --git a/copyparty/util.py b/copyparty/util.py index 67391967..d51a5531 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3611,6 +3611,7 @@ def _parsehook( chk = False fork = False jtxt = False + imp = False wait = 0.0 tout = 0.0 kill = "t" @@ -3624,6 +3625,8 @@ def _parsehook( fork = True elif arg == "j": jtxt = True + elif arg == "I": + imp = True elif arg.startswith("w"): wait = float(arg[1:]) elif arg.startswith("t"): @@ -3668,7 +3671,7 @@ def _parsehook( argv[0] = os.path.expandvars(os.path.expanduser(argv[0])) - return areq, chk, fork, jtxt, wait, sp_ka, argv + return areq, chk, imp, fork, jtxt, wait, sp_ka, argv def runihook( @@ -3678,7 +3681,7 @@ def runihook( vol: "VFS", ups: list[tuple[str, int, int, str, str, str, int, str]], ) -> bool: - _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) + _, chk, imp, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) bcmd = [sfsenc(x) for x in acmd] if acmd[0].endswith(".py"): bcmd = [sfsenc(pybin)] + bcmd @@ -3857,7 +3860,7 @@ def _runhook( txt: str, ) -> dict[str, Any]: ret = {"rc": 0} - areq, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) + areq, chk, imp, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) if areq: for ch in areq: if ch not in perms: @@ -3865,7 +3868,7 @@ def _runhook( if log: log(t % (uname, cmd, areq, perms)) return ret # fallthrough to next hook - if jtxt: + if imp or jtxt: ja = { "ap": ap, "vp": vp, @@ -3879,6 +3882,9 @@ def _runhook( "src": src, "txt": txt, } + if imp: + mod = loadpy(acmd[0], False) + return mod.main(ja) arg = json.dumps(ja) else: arg = txt or ap diff --git a/docs/devnotes.md b/docs/devnotes.md index fce97e1f..b1a9110f 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -310,6 +310,14 @@ a subset of effect types are available for a subset of hook types, to trigger indexing of files `/foo/1.txt` and `/foo/bar/2.txt`, a hook can `print(json.dumps({"idx":{"vp":["/foo/1.txt","/foo/bar/2.txt"]}}))` (and replace "idx" with "del" to delete instead) * note: paths starting with `/` are absolute URLs, but you can also do `../3.txt` relative to the destination folder of each uploaded file +### hook import + +the `I` flag runs the hook inside copyparty, which can be very useful and dangerous: + +* around 140x faster because it doesn't need to launch a new subprocess +* the hook can intentionally (or accidentally) mess with copyparty's internals + * very easy to crash things if not careful + # assumptions