add event hooks

This commit is contained in:
ed 2023-01-22 23:35:31 +00:00
parent 70f1642d0d
commit f8e3e87a52
13 changed files with 591 additions and 22 deletions

View file

@ -1,6 +1,6 @@
# ⇆🎉 copyparty # ⇆🎉 copyparty
* http file sharing hub (py2/py3) [(on PyPI)](https://pypi.org/project/copyparty/) * portable file sharing hub (py2/py3) [(on PyPI)](https://pypi.org/project/copyparty/)
* MIT-Licensed, 2019-05-26, ed @ irc.rizon.net * MIT-Licensed, 2019-05-26, ed @ irc.rizon.net
@ -75,7 +75,8 @@ try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running fro
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags * [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
* [upload events](#upload-events) - trigger a script/program on each upload * [event hooks](#event-hooks) - trigger a script/program on uploads, renames etc
* [upload events](#upload-events) - the older, more powerful approach
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed * [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
* [themes](#themes) * [themes](#themes)
* [complete examples](#complete-examples) * [complete examples](#complete-examples)
@ -163,6 +164,7 @@ recommended additional steps on debian which enable audio metadata and thumbnai
* upload * upload
* ☑ basic: plain multipart, ie6 support * ☑ basic: plain multipart, ie6 support
* ☑ [up2k](#uploading): js, resumable, multithreaded * ☑ [up2k](#uploading): js, resumable, multithreaded
* not affected by cloudflare's max-upload-size (100 MiB)
* ☑ stash: simple PUT filedropper * ☑ stash: simple PUT filedropper
* ☑ [unpost](#unpost): undo/delete accidental uploads * ☑ [unpost](#unpost): undo/delete accidental uploads
* ☑ [self-destruct](#self-destruct) (specified server-side or client-side) * ☑ [self-destruct](#self-destruct) (specified server-side or client-side)
@ -924,6 +926,8 @@ some examples,
## other flags ## other flags
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic` * `:c,magic` enables filetype detection for nameless uploads, same as `--magic`
* needs https://pypi.org/project/python-magic/ `python3 -m pip install --user -U python-magic`
* on windows grab this instead `python3 -m pip install --user -U python-magic-bin`
## database location ## database location
@ -992,9 +996,18 @@ copyparty can invoke external programs to collect additional metadata for files
if something doesn't work, try `--mtag-v` for verbose error messages if something doesn't work, try `--mtag-v` for verbose error messages
## upload events ## event hooks
trigger a script/program on each upload like so: trigger a script/program on uploads, renames etc
you can set hooks before and/or after an event happens, and currently you can hook uploads, moves/renames, and deletes
there's a bunch of flags and stuff, see `--help-hooks`
### upload events
the older, more powerful approach:
``` ```
-v /mnt/inc:inc:w:c,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send -v /mnt/inc:inc:w:c,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send
@ -1004,11 +1017,12 @@ so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, app
that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen) that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen)
note that it will only trigger on new unique files, not dupes note that this is way more complicated than the new [event hooks](#event-hooks) but this approach has the following advantages:
* non-blocking and multithreaded; doesn't hold other uploads back
* you get access to tags from FFmpeg and other mtp parsers
* only trigger on new unique files, not dupes
and it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1` note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
if this becomes popular maybe there should be a less janky way to do it actually
## hiding from google ## hiding from google

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python3
import sys
import json
import requests
from copyparty.util import humansize, quotep
_ = r"""
announces a new upload on discord
example usage as global config:
--xau f,t5,j,bin/hooks/discord-announce.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:c,xau=f,t5,j,bin/hooks/discord-announce.py
parameters explained,
f = fork; don't wait for it to finish
t5 = timeout if it's still running after 5 sec
j = provide upload information as json; not just the filename
replace "xau" with "xbu" to announce Before upload starts instead of After completion
# how to discord:
first create the webhook url; https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
then use this to design your message: https://discohook.org/
"""
def main():
WEBHOOK = "https://discord.com/api/webhooks/1234/base64"
# read info from copyparty
inf = json.loads(sys.argv[1])
vpath = inf["vp"]
filename = vpath.split("/")[-1]
url = f"https://{inf['host']}/{quotep(vpath)}"
# compose the message to discord
j = {
"title": filename,
"url": url,
"description": url.rsplit("/", 1)[0],
"color": 0x449900,
"fields": [
{"name": "Size", "value": humansize(inf["sz"])},
{"name": "User", "value": inf["user"]},
{"name": "IP", "value": inf["ip"]},
],
}
for v in j["fields"]:
v["inline"] = True
r = requests.post(WEBHOOK, json={"embeds": [j]})
print(f"discord: {r}\n", end="")
if __name__ == "__main__":
main()

30
bin/hooks/notify.py Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import sys
from plyer import notification
_ = r"""
show os notification on upload; works on windows, linux, macos
depdencies:
python3 -m pip install --user -U plyer
example usage as global config:
--xau f,bin/hooks/notify.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:c,xau=f,bin/hooks/notify.py
parameters explained,
xau = execute after upload
f = fork so it doesn't block uploads
"""
def main():
notification.notify(title="new file uploaded", message=sys.argv[1], timeout=10)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import sys
_ = r"""
reject file uploads by file extension
example usage as global config:
--xbu c,bin/hooks/reject-extension.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:c,xbu=c,bin/hooks/reject-extension.py
parameters explained,
xbu = execute before upload
c = check result, reject upload if error
"""
def main():
bad = "exe scr com pif bat ps1 jar msi"
ext = sys.argv[1].split(".")[-1]
sys.exit(1 if ext in bad.split() else 0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import sys
import magic
_ = r"""
reject file uploads by mimetype
dependencies (linux, macos):
python3 -m pip install --user -U python-magic
dependencies (windows):
python3 -m pip install --user -U python-magic-bin
example usage as global config:
--xau c,bin/hooks/reject-mimetype.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:c,xau=c,bin/hooks/reject-mimetype.py
parameters explained,
xau = execute after upload
c = check result, reject upload if error
"""
def main():
ok = ["image/jpeg", "image/png"]
mt = magic.from_file(sys.argv[1], mime=True)
print(mt)
sys.exit(1 if mt not in ok else 0)
if __name__ == "__main__":
main()

54
bin/hooks/wget.py Normal file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python3
import os
import sys
import json
import subprocess as sp
_ = r"""
use copyparty as a file downloader by POSTing URLs as
application/x-www-form-urlencoded (for example using the
message/pager function on the website)
example usage as global config:
--xm f,j,t3600,bin/hooks/wget.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:c,xm=f,j,t3600,bin/hooks/wget.py
parameters explained,
f = fork so it doesn't block uploads
j = provide message information as json; not just the text
c3 = mute all output
t3600 = timeout and kill download after 1 hour
"""
def main():
inf = json.loads(sys.argv[1])
url = inf["txt"]
if "://" not in url:
url = "https://" + url
os.chdir(inf["ap"])
name = url.split("?")[0].split("/")[-1]
tfn = "-- DOWNLOADING " + name
print(f"{tfn}\n", end="")
open(tfn, "wb").close()
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
try:
sp.check_call(cmd)
except:
t = "-- FAILED TO DONWLOAD " + name
print(f"{t}\n", end="")
open(t, "wb").close()
os.unlink(tfn)
if __name__ == "__main__":
main()

View file

@ -555,6 +555,44 @@ def get_sects():
\033[0m""" \033[0m"""
), ),
], ],
[
"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[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[0m
can be defined as --args or volflags; for example \033[36m
--xau notify-send
-v .::r:c,xau=notify-send
\033[0m
commands specified as --args are appended to volflags;
each --arg and volflag can be specified multiple times,
each command will execute in order unless one returns non-zero
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[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[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 otput
\033[0m"""
),
],
[ [
"urlform", "urlform",
"how to handle url-form POSTs", "how to handle url-form POSTs",
@ -758,6 +796,17 @@ def add_smb(ap):
ap2.add_argument("--smbvvv", action="store_true", help="verbosest") ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
def add_hooks(ap):
ap2 = ap.add_argument_group('hooks (see --help-hooks)')
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute CMD after a file upload finishes")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute CMD before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute CMD after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
def add_optouts(ap): def add_optouts(ap):
ap2 = ap.add_argument_group('opt-outs') 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("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
@ -967,6 +1016,7 @@ def run_argparse(
add_webdav(ap) add_webdav(ap)
add_smb(ap) add_smb(ap)
add_safety(ap, fk_salt) add_safety(ap, fk_salt)
add_hooks(ap)
add_optouts(ap) add_optouts(ap)
add_shutdown(ap) add_shutdown(ap)
add_ui(ap, retry) add_ui(ap, retry)

View file

@ -812,7 +812,7 @@ class AuthSrv(object):
value: Union[str, bool, list[str]], value: Union[str, bool, list[str]],
is_list: bool, is_list: bool,
) -> None: ) -> None:
if name not in ["mtp"]: if name not in ["mtp", "xbu", "xau", "xbr", "xar", "xbd", "xad", "xm"]:
flags[name] = value flags[name] = value
return return
@ -1151,8 +1151,9 @@ class AuthSrv(object):
if "mth" not in vol.flags: if "mth" not in vol.flags:
vol.flags["mth"] = self.args.mth vol.flags["mth"] = self.args.mth
# append parsers from argv to volflags # append additive args from argv to volflags
self._read_volflag(vol.flags, "mtp", self.args.mtp, True) for name in ["mtp", "xbu", "xau", "xbr", "xar", "xbd", "xad", "xm"]:
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
# d2d drops all database features for a volume # d2d drops all database features for a volume
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]: for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]:

View file

@ -63,6 +63,7 @@ from .util import (
read_socket_unbounded, read_socket_unbounded,
relchk, relchk,
ren_open, ren_open,
runhook,
hidedir, hidedir,
s3enc, s3enc,
sanitize_fn, sanitize_fn,
@ -1189,9 +1190,27 @@ class HttpCli(object):
plain = zb.decode("utf-8", "replace") plain = zb.decode("utf-8", "replace")
if buf.startswith(b"msg="): if buf.startswith(b"msg="):
plain = plain[4:] plain = plain[4:]
vfs, rem = self.asrv.vfs.get(
self.vpath, self.uname, False, False
)
xm = vfs.flags.get("xm")
if xm:
runhook(
self.log,
xm,
vfs.canonical(rem),
self.vpath,
self.host,
self.uname,
self.ip,
time.time(),
len(xm),
plain,
)
t = "urlform_dec {} @ {}\n {}\n" t = "urlform_dec {} @ {}\n {}\n"
self.log(t.format(len(plain), self.vpath, plain)) self.log(t.format(len(plain), self.vpath, plain))
except Exception as ex: except Exception as ex:
self.log(repr(ex)) self.log(repr(ex))
@ -1232,7 +1251,7 @@ class HttpCli(object):
# post_sz, sha_hex, sha_b64, remains, path, url # post_sz, sha_hex, sha_b64, remains, path, url
reader, remains = self.get_body_reader() reader, remains = self.get_body_reader()
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
rnd, _, lifetime = self.upload_flags(vfs) rnd, _, lifetime, xbu, xau = self.upload_flags(vfs)
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
fdir = vfs.canonical(rem) fdir = vfs.canonical(rem)
if lim: if lim:
@ -1332,6 +1351,24 @@ class HttpCli(object):
): ):
params["overwrite"] = "a" params["overwrite"] = "a"
if xbu:
at = time.time() - lifetime
if not runhook(
self.log,
xbu,
path,
self.vpath,
self.host,
self.uname,
self.ip,
at,
remains,
"",
):
t = "upload denied by xbu"
self.log(t, 1)
raise Pebkac(403, t)
with ren_open(fn, *open_a, **params) as zfw: with ren_open(fn, *open_a, **params) as zfw:
f, fn = zfw["orz"] f, fn = zfw["orz"]
path = os.path.join(fdir, fn) path = os.path.join(fdir, fn)
@ -1371,6 +1408,24 @@ class HttpCli(object):
fn = fn2 fn = fn2
path = path2 path = path2
at = time.time() - lifetime
if xau and not runhook(
self.log,
xau,
path,
self.vpath,
self.host,
self.uname,
self.ip,
at,
post_sz,
"",
):
t = "upload denied by xau"
self.log(t, 1)
os.unlink(path)
raise Pebkac(403, t)
vfs, rem = vfs.get_dbv(rem) vfs, rem = vfs.get_dbv(rem)
self.conn.hsrv.broker.say( self.conn.hsrv.broker.say(
"up2k.hash_file", "up2k.hash_file",
@ -1379,7 +1434,7 @@ class HttpCli(object):
rem, rem,
fn, fn,
self.ip, self.ip,
time.time() - lifetime, at,
) )
vsuf = "" vsuf = ""
@ -1572,6 +1627,8 @@ class HttpCli(object):
body["vtop"] = dbv.vpath body["vtop"] = dbv.vpath
body["ptop"] = dbv.realpath body["ptop"] = dbv.realpath
body["prel"] = vrem body["prel"] = vrem
body["host"] = self.host
body["user"] = self.uname
body["addr"] = self.ip body["addr"] = self.ip
body["vcfg"] = dbv.flags body["vcfg"] = dbv.flags
@ -1893,7 +1950,7 @@ class HttpCli(object):
self.redirect(vpath, "?edit") self.redirect(vpath, "?edit")
return True return True
def upload_flags(self, vfs: VFS) -> tuple[int, bool, int]: def upload_flags(self, vfs: VFS) -> tuple[int, bool, int, list[str], list[str]]:
srnd = self.uparam.get("rand", self.headers.get("rand", "")) srnd = self.uparam.get("rand", self.headers.get("rand", ""))
rnd = int(srnd) if srnd and not self.args.nw else 0 rnd = int(srnd) if srnd and not self.args.nw else 0
ac = self.uparam.get( ac = self.uparam.get(
@ -1907,7 +1964,7 @@ class HttpCli(object):
else: else:
lifetime = 0 lifetime = 0
return rnd, want_url, lifetime return rnd, want_url, lifetime, vfs.flags.get("xbu"), vfs.flags.get("xau")
def handle_plain_upload(self) -> bool: def handle_plain_upload(self) -> bool:
assert self.parser assert self.parser
@ -1924,7 +1981,7 @@ class HttpCli(object):
if not nullwrite: if not nullwrite:
bos.makedirs(fdir_base) bos.makedirs(fdir_base)
rnd, want_url, lifetime = self.upload_flags(vfs) rnd, want_url, lifetime, xbu, xau = self.upload_flags(vfs)
files: list[tuple[int, str, str, str, str, str]] = [] files: list[tuple[int, str, str, str, str, str]] = []
# sz, sha_hex, sha_b64, p_file, fname, abspath # sz, sha_hex, sha_b64, p_file, fname, abspath
@ -1966,6 +2023,24 @@ class HttpCli(object):
tnam = fname = os.devnull tnam = fname = os.devnull
fdir = abspath = "" fdir = abspath = ""
if xbu:
at = time.time() - lifetime
if not runhook(
self.log,
xbu,
abspath,
self.vpath,
self.host,
self.uname,
self.ip,
at,
0,
"",
):
t = "upload denied by xbu"
self.log(t, 1)
raise Pebkac(403, t)
if lim: if lim:
lim.chk_bup(self.ip) lim.chk_bup(self.ip)
lim.chk_nup(self.ip) lim.chk_nup(self.ip)
@ -2008,6 +2083,24 @@ class HttpCli(object):
files.append( files.append(
(sz, sha_hex, sha_b64, p_file or "(discarded)", fname, abspath) (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, abspath)
) )
at = time.time() - lifetime
if xau and not runhook(
self.log,
xau,
abspath,
self.vpath,
self.host,
self.uname,
self.ip,
at,
sz,
"",
):
t = "upload denied by xau"
self.log(t, 1)
os.unlink(abspath)
raise Pebkac(403, t)
dbv, vrem = vfs.get_dbv(rem) dbv, vrem = vfs.get_dbv(rem)
self.conn.hsrv.broker.say( self.conn.hsrv.broker.say(
"up2k.hash_file", "up2k.hash_file",
@ -2016,7 +2109,7 @@ class HttpCli(object):
vrem, vrem,
fname, fname,
self.ip, self.ip,
time.time() - lifetime, at,
) )
self.conn.nbyte += sz self.conn.nbyte += sz

View file

@ -14,8 +14,8 @@ from ipaddress import (
ip_network, ip_network,
) )
from .__init__ import TYPE_CHECKING from .__init__ import MACOS, TYPE_CHECKING
from .util import MACOS, Netdev, find_prefix, min_ex, spack from .util import Netdev, find_prefix, min_ex, spack
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub

View file

@ -44,6 +44,7 @@ from .util import (
ren_open, ren_open,
rmdirs, rmdirs,
rmdirs_up, rmdirs_up,
runhook,
s2hms, s2hms,
s3dec, s3dec,
s3enc, s3enc,
@ -2059,6 +2060,8 @@ class Up2k(object):
"sprs": sprs, # dontcare; finished anyways "sprs": sprs, # dontcare; finished anyways
"size": dsize, "size": dsize,
"lmod": dtime, "lmod": dtime,
"host": cj["host"],
"user": cj["user"],
"addr": ip, "addr": ip,
"at": at, "at": at,
"hash": [], "hash": [],
@ -2187,6 +2190,8 @@ class Up2k(object):
} }
# client-provided, sanitized by _get_wark: name, size, lmod # client-provided, sanitized by _get_wark: name, size, lmod
for k in [ for k in [
"host",
"user",
"addr", "addr",
"vtop", "vtop",
"ptop", "ptop",
@ -2416,6 +2421,26 @@ class Up2k(object):
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4) # self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
atomic_move(src, dst) atomic_move(src, dst)
upt = job.get("at") or time.time()
xau = self.flags[ptop].get("xau")
if xau and not runhook(
self.log,
xau,
dst,
djoin(job["vtop"], job["prel"], job["name"]),
job["host"],
job["user"],
job["addr"],
upt,
job["size"],
"",
):
t = "upload blocked by xau"
self.log(t, 1)
bos.unlink(dst)
self.registry[ptop].pop(wark, None)
raise Pebkac(403, t)
times = (int(time.time()), int(job["lmod"])) times = (int(time.time()), int(job["lmod"]))
if ANYWIN: if ANYWIN:
z1 = (dst, job["size"], times, job["sprs"]) z1 = (dst, job["size"], times, job["sprs"])
@ -2427,7 +2452,6 @@ class Up2k(object):
pass pass
z2 = [job[x] for x in "ptop wark prel name lmod size addr".split()] z2 = [job[x] for x in "ptop wark prel name lmod size addr".split()]
upt = job.get("at") or time.time()
wake_sr = False wake_sr = False
try: try:
flt = job["life"] flt = job["life"]
@ -2623,6 +2647,8 @@ class Up2k(object):
self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop)) self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop))
return 0, [], [] return 0, [], []
xbd = vn.flags.get("xbd")
xad = vn.flags.get("xad")
n_files = 0 n_files = 0
for dbv, vrem, _, adir, files, rd, vd in g: for dbv, vrem, _, adir, files, rd, vd in g:
for fn in [x[0] for x in files]: for fn in [x[0] for x in files]:
@ -2638,6 +2664,12 @@ class Up2k(object):
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/") vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
self.log("rm {}\n {}".format(vpath, abspath)) self.log("rm {}\n {}".format(vpath, abspath))
_ = dbv.get(volpath, uname, *permsets[0]) _ = dbv.get(volpath, uname, *permsets[0])
if xbd and not runhook(
self.log, xbd, abspath, vpath, "", uname, "", 0, 0, ""
):
self.log("delete blocked by xbd: {}".format(abspath), 1)
continue
with self.mutex: with self.mutex:
cur = None cur = None
try: try:
@ -2649,6 +2681,8 @@ class Up2k(object):
cur.connection.commit() cur.connection.commit()
bos.unlink(abspath) bos.unlink(abspath)
if xad:
runhook(self.log, xad, abspath, vpath, "", uname, "", 0, 0, "")
ok: list[str] = [] ok: list[str] = []
ng: list[str] = [] ng: list[str] = []
@ -2741,6 +2775,13 @@ class Up2k(object):
if bos.path.exists(dabs): if bos.path.exists(dabs):
raise Pebkac(400, "mv2: target file exists") raise Pebkac(400, "mv2: target file exists")
xbr = svn.flags.get("xbr")
xar = dvn.flags.get("xar")
if xbr and not runhook(self.log, xbr, sabs, svp, "", uname, "", 0, 0, ""):
t = "move blocked by xbr: {}".format(svp)
self.log(t, 1)
raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs)) bos.makedirs(os.path.dirname(dabs))
if bos.path.islink(sabs): if bos.path.islink(sabs):
@ -2757,6 +2798,9 @@ class Up2k(object):
with self.rescan_cond: with self.rescan_cond:
self.rescan_cond.notify_all() self.rescan_cond.notify_all()
if xar:
runhook(self.log, xar, dabs, dvp, "", uname, "", 0, 0, "")
return "k" return "k"
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem) c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem)
@ -2801,6 +2845,9 @@ class Up2k(object):
os.unlink(b1) os.unlink(b1)
if xar:
runhook(self.log, xar, dabs, dvp, "", uname, "", 0, 0, "")
return "k" return "k"
def _copy_tags( def _copy_tags(
@ -3020,6 +3067,25 @@ class Up2k(object):
# if len(job["name"].split(".")) > 8: # if len(job["name"].split(".")) > 8:
# raise Exception("aaa") # raise Exception("aaa")
xbu = self.flags[job["ptop"]].get("xbu")
ap_chk = djoin(pdir, job["name"])
vp_chk = djoin(job["vtop"], job["prel"], job["name"])
if xbu and not runhook(
self.log,
xbu,
ap_chk,
vp_chk,
job["host"],
job["user"],
job["addr"],
job["t0"],
job["size"],
"",
):
t = "upload blocked by xbu: {}".format(vp_chk)
self.log(t, 1)
raise Pebkac(403, t)
tnam = job["name"] + ".PARTIAL" tnam = job["name"] + ".PARTIAL"
if self.args.dotpart: if self.args.dotpart:
tnam = "." + tnam tnam = "." + tnam

View file

@ -6,6 +6,7 @@ import contextlib
import errno import errno
import hashlib import hashlib
import hmac import hmac
import json
import logging import logging
import math import math
import mimetypes import mimetypes
@ -362,8 +363,11 @@ class Daemon(threading.Thread):
name: Optional[str] = None, name: Optional[str] = None,
a: Optional[Iterable[Any]] = None, a: Optional[Iterable[Any]] = None,
r: bool = True, r: bool = True,
ka: Optional[dict[Any, Any]] = None,
) -> None: ) -> None:
threading.Thread.__init__(self, target=target, name=name, args=a or ()) threading.Thread.__init__(
self, target=target, name=name, args=a or (), kwargs=ka
)
self.daemon = True self.daemon = True
if r: if r:
self.start() self.start()
@ -2453,6 +2457,124 @@ def retchk(
raise Exception(t) raise Exception(t)
def _runhook(
log: "NamedLogger",
cmd: str,
ap: str,
vp: str,
host: str,
uname: str,
ip: str,
at: float,
sz: int,
txt: str,
) -> bool:
chk = False
fork = False
jtxt = False
wait = 0
tout = 0
kill = "t"
cap = 0
ocmd = cmd
while "," in cmd[:6]:
arg, cmd = cmd.split(",", 1)
if arg == "c":
chk = True
elif arg == "f":
fork = True
elif arg == "j":
jtxt = True
elif arg.startswith("w"):
wait = float(arg[1:])
elif arg.startswith("t"):
tout = float(arg[1:])
elif arg.startswith("c"):
cap = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both
elif arg.startswith("k"):
kill = arg[1:] # [t]ree [m]ain [n]one
else:
t = "hook: invalid flag {} in {}"
log(t.format(arg, ocmd))
env = os.environ.copy()
# try:
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
zsl = [str(pypath)] + [str(x) for x in sys.path if x]
pypath = str(os.pathsep.join(zsl))
env["PYTHONPATH"] = pypath
# except: if not E.ox: raise
ka = {
"env": env,
"timeout": tout,
"kill": kill,
"capture": cap,
}
if jtxt:
ja = {
"ap": ap,
"vp": vp,
"ip": ip,
"host": host,
"user": uname,
"at": at or time.time(),
"sz": sz,
"txt": txt,
}
arg = json.dumps(ja)
else:
arg = txt or ap
acmd = [cmd, arg]
if cmd.endswith(".py"):
acmd = [sys.executable] + acmd
bcmd = [fsenc(x) for x in acmd]
t0 = time.time()
if fork:
Daemon(runcmd, ocmd, [acmd], ka=ka)
else:
rc, v, err = runcmd(bcmd, **ka) # type: ignore
if chk and rc:
retchk(rc, bcmd, err, log, 5)
return False
wait -= time.time() - t0
if wait > 0:
time.sleep(wait)
return True
def runhook(
log: "NamedLogger",
cmds: list[str],
ap: str,
vp: str,
host: str,
uname: str,
ip: str,
at: float,
sz: int,
txt: str,
) -> bool:
vp = vp.replace("\\", "/")
for cmd in cmds:
try:
if not _runhook(log, cmd, ap, vp, host, uname, ip, at, sz, txt):
return False
except Exception as ex:
log("hook: {}".format(ex))
if ",c," in "," + cmd:
return False
break
return True
def gzip_orig_sz(fn: str) -> int: def gzip_orig_sz(fn: str) -> int:
with open(fsenc(fn), "rb") as f: with open(fsenc(fn), "rb") as f:
f.seek(-4, 2) f.seek(-4, 2)

View file

@ -2322,9 +2322,10 @@ function up2k_init(subtle) {
} }
var err_pend = rsp.indexOf('partial upload exists at a different') + 1, var err_pend = rsp.indexOf('partial upload exists at a different') + 1,
err_plug = rsp.indexOf('upload blocked by x') + 1,
err_dupe = rsp.indexOf('upload rejected, file already exists') + 1; err_dupe = rsp.indexOf('upload rejected, file already exists') + 1;
if (err_pend || err_dupe) { if (err_pend || err_plug || err_dupe) {
err = rsp; err = rsp;
ofs = err.indexOf('\n/'); ofs = err.indexOf('\n/');
if (ofs !== -1) { if (ofs !== -1) {
@ -2431,6 +2432,14 @@ function up2k_init(subtle) {
function orz(xhr) { function orz(xhr) {
var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + ''; var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + '';
if (txt.indexOf('upload blocked by x') + 1) {
apop(st.busy.upload, upt);
apop(t.postlist, npart);
pvis.seth(t.n, 1, "ERROR");
pvis.seth(t.n, 2, txt.split(/\n/)[0]);
pvis.move(t.n, 'ng');
return;
}
if (xhr.status == 200) { if (xhr.status == 200) {
pvis.prog(t, npart, cdr - car); pvis.prog(t, npart, cdr - car);
st.bytes.finished += cdr - car; st.bytes.finished += cdr - car;