add hook side-effects; closes #86

hooks can now interrupt or redirect actions, and initiate
related actions, by printing json on stdout with commands

mainly to mitigate limitations such as sharex/sharex#3992

xbr/xau can redirect uploads to other destinations with `reloc`
and most hooks can initiate indexing or deletion of additional
files by giving a list of vpaths in json-keys `idx` or `del`

there are limitations;
* xbu/xau effects don't apply to ftp, tftp, smb
* xau will intentionally fail if a reloc destination exists
* xau effects do not apply to up2k

also provides more details for hooks:
* xbu/xau: basic-uploader vpath with filename
* xbr/xar: add client ip
This commit is contained in:
ed
2024-08-11 14:52:32 +00:00
parent 20669c73d3
commit 6c94a63f1c
15 changed files with 799 additions and 181 deletions
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
import shutil
import tempfile
import unittest
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from tests import util as tu
from tests.util import Cfg
def hdr(query):
h = "GET /{} HTTP/1.1\r\nPW: o\r\nConnection: close\r\n\r\n"
return h.format(query).encode("utf-8")
class TestHooks(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def reset(self):
td = os.path.join(self.td, "vfs")
if os.path.exists(td):
shutil.rmtree(td)
os.mkdir(td)
os.chdir(td)
return td
def test(self):
vcfg = ["a/b/c/d:c/d:A", "a:a:r"]
scenarios = (
('{"vp":"x/y"}', "c/d/a.png", "c/d/x/y/a.png"),
('{"vp":"x/y"}', "c/d/e/a.png", "c/d/e/x/y/a.png"),
('{"vp":"../x/y"}', "c/d/e/a.png", "c/d/x/y/a.png"),
('{"ap":"x/y"}', "c/d/a.png", "c/d/x/y/a.png"),
('{"ap":"x/y"}', "c/d/e/a.png", "c/d/e/x/y/a.png"),
('{"ap":"../x/y"}', "c/d/e/a.png", "c/d/x/y/a.png"),
('{"ap":"../x/y"}', "c/d/a.png", "a/b/c/x/y/a.png"),
('{"fn":"b.png"}', "c/d/a.png", "c/d/b.png"),
('{"vp":"x","fn":"b.png"}', "c/d/a.png", "c/d/x/b.png"),
)
for x in scenarios:
print("\n\n\n", x)
hooktxt, url_up, url_dl = x
for hooktype in ("xbu", "xau"):
for upfun in (self.put, self.bup):
self.reset()
self.makehook("""print('{"reloc":%s}')""" % (hooktxt,))
ka = {hooktype: ["j,c1,h.py"]}
self.args = Cfg(v=vcfg, a=["o:o"], e2d=True, **ka)
self.asrv = AuthSrv(self.args, self.log)
h, b = upfun(url_up)
self.assertIn("201 Created", h)
h, b = self.curl(url_dl)
self.assertEqual(b, "ok %s\n" % (url_up))
def makehook(self, hs):
with open("h.py", "wb") as f:
f.write(hs.encode("utf-8"))
def put(self, url):
buf = "PUT /{0} HTTP/1.1\r\nPW: o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
buf = buf.format(url, len(url) + 4).encode("utf-8")
print("PUT -->", buf)
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf)
HttpCli(conn).run()
ret = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("PUT <--", ret)
return ret
def bup(self, url):
hdr = "POST /%s HTTP/1.1\r\nPW: o\r\nConnection: close\r\nContent-Type: multipart/form-data; boundary=XD\r\nContent-Length: %d\r\n\r\n"
bdy = '--XD\r\nContent-Disposition: form-data; name="act"\r\n\r\nbput\r\n--XD\r\nContent-Disposition: form-data; name="f"; filename="%s"\r\n\r\n'
ftr = "\r\n--XD--\r\n"
try:
url, fn = url.rsplit("/", 1)
except:
fn = url
url = ""
buf = (bdy % (fn,) + "ok %s/%s\n" % (url, fn) + ftr).encode("utf-8")
buf = (hdr % (url, len(buf))).encode("utf-8") + buf
print("PoST -->", buf)
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf)
HttpCli(conn).run()
ret = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("POST <--", ret)
return ret
def curl(self, url, binary=False):
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url))
HttpCli(conn).run()
if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)
+14 -3
View File
@@ -68,6 +68,13 @@ def chkcmd(argv):
def get_ramdisk():
def subdir(top):
for d in os.listdir(top):
if not d.startswith("cptd-"):
continue
p = os.path.join(top, d)
st = os.stat(p)
if time.time() - st.st_mtime > 300:
shutil.rmtree(p)
ret = os.path.join(top, "cptd-{}".format(os.getpid()))
shutil.rmtree(ret, True)
os.mkdir(ret)
@@ -111,10 +118,10 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver xdev xlink xvol"
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol"
ka.update(**{k: False for k in ex.split()})
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
ex = "dotpart dotsrch hook_v no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
@@ -178,6 +185,10 @@ class Cfg(Namespace):
class NullBroker(object):
def __init__(self, args, asrv):
self.args = args
self.asrv = asrv
def say(self, *args):
pass
@@ -213,7 +224,7 @@ class VHttpSrv(object):
self.asrv = asrv
self.log = log
self.broker = NullBroker()
self.broker = NullBroker(args, asrv)
self.prism = None
self.bans = {}
self.nreq = 0