hooks: import-flag

This commit is contained in:
ed 2025-10-04 13:32:26 +00:00
parent a0f8f794a8
commit 41ed559faa
4 changed files with 79 additions and 4 deletions

54
bin/hooks/import-me.py Normal file
View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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