diff --git a/README.md b/README.md index cff2c2c0..12c99aaa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⇆🎉 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 @@ -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 * [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 - * [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 * [themes](#themes) * [complete examples](#complete-examples) @@ -163,6 +164,7 @@ recommended additional steps on debian which enable audio metadata and thumbnai * upload * ☑ basic: plain multipart, ie6 support * ☑ [up2k](#uploading): js, resumable, multithreaded + * not affected by cloudflare's max-upload-size (100 MiB) * ☑ stash: simple PUT filedropper * ☑ [unpost](#unpost): undo/delete accidental uploads * ☑ [self-destruct](#self-destruct) (specified server-side or client-side) @@ -924,6 +926,8 @@ some examples, ## other flags * `: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 @@ -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 -## 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 @@ -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) -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` - -if this becomes popular maybe there should be a less janky way to do it actually +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` ## hiding from google diff --git a/bin/hooks/discord-announce.py b/bin/hooks/discord-announce.py new file mode 100644 index 00000000..e31668c8 --- /dev/null +++ b/bin/hooks/discord-announce.py @@ -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() diff --git a/bin/hooks/notify.py b/bin/hooks/notify.py new file mode 100644 index 00000000..8541e479 --- /dev/null +++ b/bin/hooks/notify.py @@ -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() diff --git a/bin/hooks/reject-extension.py b/bin/hooks/reject-extension.py new file mode 100644 index 00000000..fca74094 --- /dev/null +++ b/bin/hooks/reject-extension.py @@ -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() diff --git a/bin/hooks/reject-mimetype.py b/bin/hooks/reject-mimetype.py new file mode 100644 index 00000000..d7bc0fe7 --- /dev/null +++ b/bin/hooks/reject-mimetype.py @@ -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() diff --git a/bin/hooks/wget.py b/bin/hooks/wget.py new file mode 100644 index 00000000..b048c060 --- /dev/null +++ b/bin/hooks/wget.py @@ -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() diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 90b4c626..d0ed16b8 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -555,6 +555,44 @@ def get_sects(): \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", "how to handle url-form POSTs", @@ -758,6 +796,17 @@ def add_smb(ap): 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): ap2 = ap.add_argument_group('opt-outs') 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_smb(ap) add_safety(ap, fk_salt) + add_hooks(ap) add_optouts(ap) add_shutdown(ap) add_ui(ap, retry) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index c2cedbea..0876d759 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -812,7 +812,7 @@ class AuthSrv(object): value: Union[str, bool, list[str]], is_list: bool, ) -> None: - if name not in ["mtp"]: + if name not in ["mtp", "xbu", "xau", "xbr", "xar", "xbd", "xad", "xm"]: flags[name] = value return @@ -1151,8 +1151,9 @@ class AuthSrv(object): if "mth" not in vol.flags: vol.flags["mth"] = self.args.mth - # append parsers from argv to volflags - self._read_volflag(vol.flags, "mtp", self.args.mtp, True) + # append additive args from argv to volflags + 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 for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 30c68518..1e2377ed 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -63,6 +63,7 @@ from .util import ( read_socket_unbounded, relchk, ren_open, + runhook, hidedir, s3enc, sanitize_fn, @@ -1189,9 +1190,27 @@ class HttpCli(object): plain = zb.decode("utf-8", "replace") if buf.startswith(b"msg="): 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" self.log(t.format(len(plain), self.vpath, plain)) + except Exception as ex: self.log(repr(ex)) @@ -1232,7 +1251,7 @@ class HttpCli(object): # post_sz, sha_hex, sha_b64, remains, path, url reader, remains = self.get_body_reader() 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 fdir = vfs.canonical(rem) if lim: @@ -1332,6 +1351,24 @@ class HttpCli(object): ): 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: f, fn = zfw["orz"] path = os.path.join(fdir, fn) @@ -1371,6 +1408,24 @@ class HttpCli(object): fn = fn2 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) self.conn.hsrv.broker.say( "up2k.hash_file", @@ -1379,7 +1434,7 @@ class HttpCli(object): rem, fn, self.ip, - time.time() - lifetime, + at, ) vsuf = "" @@ -1572,6 +1627,8 @@ class HttpCli(object): body["vtop"] = dbv.vpath body["ptop"] = dbv.realpath body["prel"] = vrem + body["host"] = self.host + body["user"] = self.uname body["addr"] = self.ip body["vcfg"] = dbv.flags @@ -1893,7 +1950,7 @@ class HttpCli(object): self.redirect(vpath, "?edit") 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", "")) rnd = int(srnd) if srnd and not self.args.nw else 0 ac = self.uparam.get( @@ -1907,7 +1964,7 @@ class HttpCli(object): else: 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: assert self.parser @@ -1924,7 +1981,7 @@ class HttpCli(object): if not nullwrite: 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]] = [] # sz, sha_hex, sha_b64, p_file, fname, abspath @@ -1966,6 +2023,24 @@ class HttpCli(object): tnam = fname = os.devnull 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: lim.chk_bup(self.ip) lim.chk_nup(self.ip) @@ -2008,6 +2083,24 @@ class HttpCli(object): files.append( (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) self.conn.hsrv.broker.say( "up2k.hash_file", @@ -2016,7 +2109,7 @@ class HttpCli(object): vrem, fname, self.ip, - time.time() - lifetime, + at, ) self.conn.nbyte += sz diff --git a/copyparty/multicast.py b/copyparty/multicast.py index 9baed3df..105089fc 100644 --- a/copyparty/multicast.py +++ b/copyparty/multicast.py @@ -14,8 +14,8 @@ from ipaddress import ( ip_network, ) -from .__init__ import TYPE_CHECKING -from .util import MACOS, Netdev, find_prefix, min_ex, spack +from .__init__ import MACOS, TYPE_CHECKING +from .util import Netdev, find_prefix, min_ex, spack if TYPE_CHECKING: from .svchub import SvcHub diff --git a/copyparty/up2k.py b/copyparty/up2k.py index e56ea931..b83da3bf 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -44,6 +44,7 @@ from .util import ( ren_open, rmdirs, rmdirs_up, + runhook, s2hms, s3dec, s3enc, @@ -2059,6 +2060,8 @@ class Up2k(object): "sprs": sprs, # dontcare; finished anyways "size": dsize, "lmod": dtime, + "host": cj["host"], + "user": cj["user"], "addr": ip, "at": at, "hash": [], @@ -2187,6 +2190,8 @@ class Up2k(object): } # client-provided, sanitized by _get_wark: name, size, lmod for k in [ + "host", + "user", "addr", "vtop", "ptop", @@ -2416,6 +2421,26 @@ class Up2k(object): # self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4) 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"])) if ANYWIN: z1 = (dst, job["size"], times, job["sprs"]) @@ -2427,7 +2452,6 @@ class Up2k(object): pass z2 = [job[x] for x in "ptop wark prel name lmod size addr".split()] - upt = job.get("at") or time.time() wake_sr = False try: flt = job["life"] @@ -2623,6 +2647,8 @@ class Up2k(object): self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop)) return 0, [], [] + xbd = vn.flags.get("xbd") + xad = vn.flags.get("xad") n_files = 0 for dbv, vrem, _, adir, files, rd, vd in g: for fn in [x[0] for x in files]: @@ -2638,6 +2664,12 @@ class Up2k(object): vpath = "{}/{}".format(dbv.vpath, volpath).strip("/") self.log("rm {}\n {}".format(vpath, abspath)) _ = 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: cur = None try: @@ -2649,6 +2681,8 @@ class Up2k(object): cur.connection.commit() bos.unlink(abspath) + if xad: + runhook(self.log, xad, abspath, vpath, "", uname, "", 0, 0, "") ok: list[str] = [] ng: list[str] = [] @@ -2741,6 +2775,13 @@ class Up2k(object): if bos.path.exists(dabs): 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)) if bos.path.islink(sabs): @@ -2757,6 +2798,9 @@ class Up2k(object): with self.rescan_cond: self.rescan_cond.notify_all() + if xar: + runhook(self.log, xar, dabs, dvp, "", uname, "", 0, 0, "") + return "k" c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem) @@ -2801,6 +2845,9 @@ class Up2k(object): os.unlink(b1) + if xar: + runhook(self.log, xar, dabs, dvp, "", uname, "", 0, 0, "") + return "k" def _copy_tags( @@ -3020,6 +3067,25 @@ class Up2k(object): # if len(job["name"].split(".")) > 8: # 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" if self.args.dotpart: tnam = "." + tnam diff --git a/copyparty/util.py b/copyparty/util.py index ad65992b..c2ced735 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -6,6 +6,7 @@ import contextlib import errno import hashlib import hmac +import json import logging import math import mimetypes @@ -362,8 +363,11 @@ class Daemon(threading.Thread): name: Optional[str] = None, a: Optional[Iterable[Any]] = None, r: bool = True, + ka: Optional[dict[Any, Any]] = 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 if r: self.start() @@ -2453,6 +2457,124 @@ def retchk( 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: with open(fsenc(fn), "rb") as f: f.seek(-4, 2) diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index e85918fa..c7bf5ec5 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -2322,9 +2322,10 @@ function up2k_init(subtle) { } 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; - if (err_pend || err_dupe) { + if (err_pend || err_plug || err_dupe) { err = rsp; ofs = err.indexOf('\n/'); if (ofs !== -1) { @@ -2431,6 +2432,14 @@ function up2k_init(subtle) { function orz(xhr) { 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) { pvis.prog(t, npart, cdr - car); st.bytes.finished += cdr - car;