From 889bd3242a03278a6e51e4578bcee8ae6fa03b6a Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 30 Nov 2025 19:29:09 +0000 Subject: [PATCH 01/67] hooks: retcode 100, zmq json; hooks returning exitcode 0 will: * run the next hook, if any * allow the original action, unless successive hook opposes hooks returning exitcode 100 will: * abort running successive hooks * allow the original action hooks returning anything other than 0 or 100 will: * abort running successive hooks * REJECT the original action zmq can now respond with json; a dict with "rc", "rejectmsg", "reloc" and so on, just like other hooks replying with json --- bin/hooks/README.md | 5 ++++ bin/zmq-recv.py | 2 ++ copyparty/__main__.py | 3 ++- copyparty/ftpd.py | 2 +- copyparty/httpcli.py | 60 ++++++++++++++++++++++--------------------- copyparty/smbd.py | 2 +- copyparty/tftpd.py | 2 +- copyparty/up2k.py | 35 +++++++++++++++---------- copyparty/util.py | 31 ++++++++++++++-------- tests/test_hooks.py | 35 +++++++++++++++++++++++++ 10 files changed, 121 insertions(+), 56 deletions(-) diff --git a/bin/hooks/README.md b/bin/hooks/README.md index 9c18c55b..c0ce7bf1 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -4,6 +4,11 @@ these programs either take zero arguments, or a filepath (the affected file), or run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban) +in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check") then its exit-code controls the action that launched the hook: +* exit-code `0` = allow the action, and/or continue running the next hook +* exit-code `100` = allow the action, and stop running any remaining consecutive hooks +* anything else = reject/prevent the original action, and don't run the remaining hooks + > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead diff --git a/bin/zmq-recv.py b/bin/zmq-recv.py index 72fa24f7..93e0c168 100755 --- a/bin/zmq-recv.py +++ b/bin/zmq-recv.py @@ -61,6 +61,8 @@ def rep_server(): print("copyparty says %r" % (sck.recv_string(),)) reply = b"thx" # reply = b"return 1" # non-zero to block an upload + # reply = b'{"rc":1}' # or as json, that's fine too + # reply = b'{"rejectmsg":"naw dude"}' # or custom message sck.send(reply) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f8dcffb1..d6d30f1a 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -809,7 +809,8 @@ def get_sects(): \033[0m hooks specified as commandline --args are appended to volflags; each commandline --arg and volflag can be specified multiple times, - each hook will execute in order unless one returns non-zero + each hook will execute in order unless one returns non-zero, or + "100" which means "stop daisychaining and return 0 (success/OK)" optionally prefix the command with comma-sep. flags similar to -mtp: diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 12285d64..fd5d0349 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -515,7 +515,7 @@ class FtpHandler(FTPHandler): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "Upload blocked by xbu server config: %r" % (vp,) self.respond("550 %s" % (t,), logging.info) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6e547863..ee84d45e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -913,29 +913,31 @@ class HttpCli(object): return False xban = self.vn.flags.get("xban") - if not xban or not runhook( - self.log, - self.conn.hsrv.broker, - None, - "xban", - xban, - self.vn.canonical(self.rem), - self.vpath, - self.host, - self.uname, - "", - time.time(), - 0, - self.ip, - time.time(), - [reason, reason], - ): - self.log("client banned: %s" % (descr,), 1) - self.conn.hsrv.bans[ip] = bonk - self.conn.hsrv.nban += 1 - return True + if xban: + hr = runhook( + self.log, + self.conn.hsrv.broker, + None, + "xban", + xban, + self.vn.canonical(self.rem), + self.vpath, + self.host, + self.uname, + "", + time.time(), + 0, + self.ip, + time.time(), + [reason, reason], + ) + if hr.get("rv") == 0: + return False - return False + self.log("client banned: %s" % (descr,), 1) + self.conn.hsrv.bans[ip] = bonk + self.conn.hsrv.nban += 1 + return True def is_banned(self) -> bool: if not self.conn.bans: @@ -2386,7 +2388,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp,) self.log(t, 1) @@ -2521,7 +2523,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" % (vp,) self.log(t, 1) @@ -3359,7 +3361,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "new-md blocked by " + hn + " server config: %r" t = t % (vjoin(vfs.vpath, rem),) @@ -3530,7 +3532,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" t = t % (vjoin(upload_vpath, fname),) @@ -3637,7 +3639,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" t = t % (vjoin(upload_vpath, fname),) @@ -3950,7 +3952,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "save blocked by xbu server config" self.log(t, 1) @@ -3998,7 +4000,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "save blocked by xau server config" self.log(t, 1) diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 40e4f088..0d48a733 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -265,7 +265,7 @@ class SMB(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "blocked by xbu server config: %r" % (vpath,) yeet(t) diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 88fd7782..e1f75c8b 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -382,7 +382,7 @@ class Tftpd(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vpath,) yeet(t) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 4f33bb0c..a2c3b233 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3307,7 +3307,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" t = t % (vp,) @@ -4003,7 +4003,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" t = t % (djoin(vtop, rd, fn),) @@ -4221,7 +4221,7 @@ class Up2k(object): _ = dbv.get(volpath, uname, *permsets[0]) if xbd: - if not runhook( + hr = runhook( self.log, None, self, @@ -4237,9 +4237,12 @@ class Up2k(object): ip, time.time(), None, - ): - t = "delete blocked by xbd server config: %r" - self.log(t % (abspath,), 1) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "delete blocked by xbd server config: %r" % (abspath,) + self.log(t, 1) continue n_files += 1 @@ -4389,7 +4392,7 @@ class Up2k(object): xbc = svn.flags.get("xbc") xac = dvn.flags.get("xac") if xbc: - if not runhook( + hr = runhook( self.log, None, self, @@ -4405,8 +4408,11 @@ class Up2k(object): ip, time.time(), None, - ): - t = "copy blocked by xbr server config: %r" % (svp,) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "copy blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) @@ -4641,7 +4647,7 @@ class Up2k(object): xbr = svn.flags.get("xbr") xar = dvn.flags.get("xar") if xbr: - if not runhook( + hr = runhook( self.log, None, self, @@ -4657,8 +4663,11 @@ class Up2k(object): ip, time.time(), None, - ): - t = "move blocked by xbr server config: %r" % (svp,) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "move blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) @@ -5163,7 +5172,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp_chk,) self.log(t, 1) diff --git a/copyparty/util.py b/copyparty/util.py index 3c9ed2ab..7290dd9e 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3930,7 +3930,13 @@ def _runhook( zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka) if zi: raise Exception("zmq says %d" % (zi,)) - return {"rc": 0, "stdout": zs} + try: + ret = json.loads(zs) + if "rc" not in ret: + ret["rc"] = 0 + return ret + except: + return {"rc": 0, "stdout": zs} if sin: sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace") @@ -3949,20 +3955,23 @@ def _runhook( rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore if chk and rc: ret["rc"] = rc - retchk(rc, bcmd, err, log, 5) + zi = 0 if rc == 100 else rc + retchk(zi, bcmd, err, log, 5) else: try: ret = json.loads(v) except: - ret = {} + pass try: if "stdout" not in ret: ret["stdout"] = v + if "stderr" not in ret: + ret["stderr"] = err if "rc" not in ret: ret["rc"] = rc except: - ret = {"rc": rc, "stdout": v} + ret = {"rc": rc, "stdout": v, "stderr": err} if wait: wait -= time.time() - t0 @@ -3994,6 +4003,7 @@ def runhook( verbose = args.hook_v vp = vp.replace("\\", "/") ret = {"rc": 0} + stop = False for cmd in cmds: try: hr = _runhook( @@ -4001,8 +4011,6 @@ def runhook( ) if verbose and log: log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6) - if not hr: - return {} for k, v in hr.items(): if k in ("idx", "del") and v: if broker: @@ -4013,17 +4021,20 @@ def runhook( elif k == "reloc" and v: # idk, just take the last one ig ret["reloc"] = v + elif k == "rc" and v: + stop = True + ret[k] = 0 if v == 100 else v elif k in ret: - if k == "rc" and v: - ret[k] = v - elif k == "stdout" and v and not ret[k]: + if k == "stdout" and v and not ret[k]: ret[k] = v else: ret[k] = v except Exception as ex: (log or print)("hook: %r, %s" % (ex, ex)) if ",c," in "," + cmd: - return {} + return {"rc": 1} + break + if stop: break return ret diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 63a525df..6b366023 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -78,6 +78,41 @@ class TestHooks(tu.TC): h, b = self.curl(url_dl) self.assertEqual(b, "ok %s\n" % (url_up)) + def test2(self): + hooktxt = "import sys\nopen('h%d','wb').close()\nsys.exit(%d)\n" + for hooktype in ("xbu", "xau"): + for upfun in (self.put, self.bup): + self.reset() + for n in [0, 1, 100]: + with open("h%d.py" % (n,), "wb") as f: + f.write((hooktxt % (n, n)).encode("utf-8")) + vcfg = [ + "012:012:A:c,H=c,h0.py:c,H=c,h1.py:c,H=c,h100.py", + "021:021:A:c,H=c,h0.py:c,H=c,h100.py:c,H=c,h1.py", + "120:120:A:c,H=c,h1.py:c,H=c,h100.py:c,H=c,h0.py", + "30:30:A:c,H=c,enoent.py:c,H=c,h100.py", # not-exist + ] + vcfg = [x.replace("H", hooktype) for x in vcfg] + self.args = Cfg(v=vcfg, a=["o:o"], e2d=True) + self.asrv = AuthSrv(self.args, self.log) + self.cinit() + scenarios = ( + ("012", False, True, True, False), + ("021", True, True, False, True), + ("120", False, False, True, False), + ("30", False, False, False, False), + ) + for (vp, ok, h0, h1, h2) in scenarios: + for zs in ("h0", "h1", "h100"): + if os.path.exists(zs): + os.unlink(zs) + vp = "%s/f" % (vp,) + h, b = upfun(vp) + self.assertEqual(ok, os.path.exists(vp)) + self.assertEqual(h0, os.path.exists("h0")) + self.assertEqual(h1, os.path.exists("h1")) + self.assertEqual(h2, os.path.exists("h100")) + def makehook(self, hs): with open("h.py", "wb") as f: f.write(hs.encode("utf-8")) From f4d67ff03158429ebd5bb1ed9970e88f128328f0 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 30 Nov 2025 19:59:57 +0000 Subject: [PATCH 02/67] fix double pathsep in ongoing-xfer links --- copyparty/httpcli.py | 18 ++++++++++-------- copyparty/web/splash.html | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index ee84d45e..1d0026a5 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5269,10 +5269,11 @@ class HttpCli(object): fdone = max(0.001, 1 - rem) td = max(0.1, now - t0) rd, fn = vsplit(vp.replace(os.sep, "/")) - if not rd: - rd = "/" - erd = quotep(rd) - rds = rd.replace("/", " / ") + if rd: + rds = rd.replace("/", " / ") + erd = "/%s/" % (quotep(rd),) + else: + erd = rds = "/" spd = humansize(sz * fdone / td, True) + "/s" eta = s2hms((td / fdone) - td, True) if rem < 1 else "--" idle = s2hms(now - poke, True) @@ -5299,10 +5300,11 @@ class HttpCli(object): for t0, t1, sent, sz, vp, dl_id, uname in dl_list: td = max(0.1, now - t0) rd, fn = vsplit(vp) - if not rd: - rd = "/" - erd = quotep(rd) - rds = rd.replace("/", " / ") + if rd: + rds = rd.replace("/", " / ") + erd = "/%s/" % (quotep(rd),) + else: + erd = rds = "/" spd = humansize(sent / td, True) + "/s" hsent = humansize(sent, True) idle = s2hms(now - t1, True) diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 848b8905..9b642b9b 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -42,7 +42,7 @@ %speedetaidledirfile {%- for u in ups %} - {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[5]|e }}{{ u[6]|e }} + {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[5]|e }}{{ u[6]|e }} {%- endfor %} @@ -54,7 +54,7 @@ %sentspeedetaidledirfile {%- for u in dls %} - {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[4] }}{{ u[5] }}{{ u[7]|e }}{{ u[8] }} + {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[4] }}{{ u[5] }}{{ u[7]|e }}{{ u[8] }} {%- endfor %} From cedfc444206d72d4ee68ccfaf202c2333465ace2 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 30 Nov 2025 20:06:56 +0000 Subject: [PATCH 03/67] panic if --shr overlaps with volumes --- copyparty/authsrv.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index eb853b52..263912d9 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3131,7 +3131,10 @@ class AuthSrv(object): self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1) vols.append(vol) if shr in vfs.all_nodes: - self.log("BUG: %s found in all_nodes" % (shr,), 1) + t = "invalid config: a volume is overlapping with the --shr global-option (/%s)" + t = t % (shr,) + self.log(t, 1) + raise Exception(t) for vol in vols: dbv = vol.get_dbv("")[0] From acde21d484886ba53474c8551dce016b0d87fbb6 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 30 Nov 2025 20:36:32 +0000 Subject: [PATCH 04/67] fix controlpanel greeting in early responses; responses sent early during request processing (primarily for invalid requests) would display the username " " rater than "*" in the controlpanel, in one case leading to user confusion --- copyparty/httpcli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 1d0026a5..86d224c8 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -247,8 +247,8 @@ class HttpCli(object): self.dl_id = "" self.gctx = " " # additional context for garda self.trailing_slash = True - self.uname = " " - self.pw = " " + self.uname = "*" + self.pw = "" self.rvol = self.wvol = self.avol = empty_stringlist self.do_log = True self.can_read = False From dba7c5d4d5ead25447bdb610ac9aea8291ced415 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 30 Nov 2025 22:14:26 +0000 Subject: [PATCH 05/67] iOS: bbox: fix video scrubbing; unlike android, iOS does not eat touch-events in the video controls, so it would switch to the prev/next media on seek instead of seek --- copyparty/web/baguettebox.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index e6acd2d3..6686e732 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -60,6 +60,15 @@ window.baguetteBox = (function () { hideOverlay(); }; + var vtouch = function (e) { + var v = vid(), + bv = v.getBoundingClientRect(), + tp = e.changedTouches[0]; + + if (bv.bottom - tp.clientY < 90) + touchFlag = true; + }; + var touchstartHandler = function (e) { touch.count = e.touches.length; if (touch.count > 1) @@ -822,6 +831,7 @@ window.baguetteBox = (function () { if (v == keep) continue; + unbind(v, 'touchstart', vtouch, nonPassiveEvent); unbind(v, 'error', lerr); v.src = ''; v.load(); @@ -1250,6 +1260,7 @@ window.baguetteBox = (function () { setloop(); } } + bind(v, 'touchstart', vtouch, nonPassiveEvent); } selbg(); mp_ctl(); From a31bfe6b2b01de178d8dd17e65cc8d9689ce0cdd Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 06:26:08 +0100 Subject: [PATCH 06/67] update security policy Signed-off-by: ed --- SECURITY.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 964b96f3..119464e5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,7 @@ # Security Policy -if you hit something extra juicy pls let me know on either of the following +if you hit something extra juicy pls let me know on one of the following: * email -- `copyparty@ocv.ze` except `ze` should be `me` -* [mastodon dm](https://layer8.space/@tripflag) -- `@tripflag@layer8.space` * [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated -* [twitter dm](https://twitter.com/tripflag) (if im somehow not banned yet) no bug bounties sorry! all i can offer is greetz in the release notes From ba7387209affbd8f4b9eb85c152526b6b48b0f0d Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 14:40:18 +0000 Subject: [PATCH 07/67] github: update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 11 ++++++++--- .github/ISSUE_TEMPLATE/feature_request.md | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e0b65771..63d78c8b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,10 +7,9 @@ assignees: '9001' --- -NOTE: + ### Describe the bug a description of what the bug is @@ -45,5 +44,11 @@ if the issue is possibly on the client-side, then mention some of the following: * OS version: * browser version: +### The rest of the stack +if you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.) + +### Server log +if the issue might be server-related, include everything that appears in the copyparty log during startup, and also anything else you think might be relevant + ### Additional context any other context about the problem here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2df6b4bb..390943c9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,9 +7,9 @@ assignees: '9001' --- -NOTE: + **is your feature request related to a problem? Please describe.** a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]` From 04ac7fbd21741e5fb9dd8471e7b224630385aa1d Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 16:32:33 +0000 Subject: [PATCH 08/67] shares: remove delete-permission (closes #1023); until now, shares could be created with permissions read/write/delete (any combination thereof), however the delete option was never fully implemented and dysfunctional, hence now removed using vn0/rem0 throughout _handle_rm would almost be sufficient however the primary concern is ensuring integrity of metadata tables, and _forget_file expects a dbv rather than the share's vn --- copyparty/httpcli.py | 3 +++ copyparty/web/browser.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 86d224c8..395e7254 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6231,6 +6231,9 @@ class HttpCli(object): self.log("unpost was denied" + BADXFF, 1) raise Pebkac(403, "the delete feature is disabled in server config") + if not unpost and self.vn.shr_src: + raise Pebkac(403, "files in shares can only be deleted with unpost") + if not req: req = [self.vpath] elif self.is_vproxied: diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a29016c3..093467af 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -3825,7 +3825,7 @@ var fileman = (function () { 'perms', ]; for (var a = 0; a < perms.length; a++) - if (!has(['admin', 'move'], perms[a])) + if (!has(['admin', 'move', 'delete'], perms[a])) html.push('' + perms[a] + ''); if (has(perms, 'write')) From 278a0d8548548f066da40230a0473a887026847e Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 17:23:47 +0000 Subject: [PATCH 09/67] md: rewrite links to open in viewer; closes #972 --- copyparty/web/md.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/copyparty/web/md.js b/copyparty/web/md.js index 66be69bb..7889f4f3 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -239,6 +239,12 @@ function convert_markdown(md_text, dest_dom) { var href = nodes[a].getAttribute('href'); var txt = nodes[a].innerHTML; + if (/\.[Mm][Dd]$/.test(href)) { + var o = new URL(href, location.href).origin; + if (!o || o == location.origin) + nodes[a].href = href + '?v'; + } + if (!txt) nodes[a].textContent = href; else if (href !== txt && !nodes[a].className) From fcc1bdfbf5b967be63a5fe39cfb5b6081857b803 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 17:38:19 +0000 Subject: [PATCH 10/67] decode ansi-colors in .txt/nfo files; closes #1064 --- copyparty/web/browser.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 093467af..f0f50e5a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4771,6 +4771,7 @@ var showfile = (function () { '.log': 'ans', '.m': 'matlab', '.moon': 'moonscript', + '.nfo': 'ans', '.patch': 'diff', '.ps1': 'powershell', '.psm1': 'powershell', @@ -4778,6 +4779,7 @@ var showfile = (function () { '.rs': 'rust', '.sh': 'bash', '.service': 'systemd', + '.txt': 'ans', '.vb': 'vbnet', '.v': 'verilog', '.vert': 'glsl', @@ -4791,7 +4793,8 @@ var showfile = (function () { var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nim py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig'; x = x.split(/ +/g); for (var a = 0; a < x.length; a++) - r.map["." + x[a]] = x[a]; + if (!r.map["." + x[a]]) + r.map["." + x[a]] = x[a]; r.sname = function (srch) { return srch.split(/[?&]doc=/)[1].split('&')[0]; From a9174e5deeab0964b04a2f22f712a50d19527c1c Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 1 Dec 2025 19:02:03 +0000 Subject: [PATCH 11/67] ui-option to force-download files (closes #1058); * button "dl" in settings UI (always takes precedence) * global-option and/or volflag "dlni" * url-parameter ?dlni or ?dlni=0 the preference is applied per-volume when navigating between folders, unless the settings-button has been toggled, which overrides that --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 5 ++++- copyparty/cfg.py | 2 ++ copyparty/web/browser.js | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d6d30f1a..7b5c32a1 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1793,6 +1793,7 @@ def add_ui(ap, retry: int): ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)") ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions") ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)") + ap2.add_argument("--dlni", action="store_true", help="force download (don't show inline) when files are clicked (volflag:dlni)") ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 263912d9..de6dc18b 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3049,6 +3049,8 @@ class AuthSrv(object): vn.js_ls = { "idx": "e2d" in vf, "itag": "e2t" in vf, + "dlni": "dlni" in vf, + "dgrid": "grid" in vf, "dnsort": "nsort" in vf, "dhsortn": vf["hsortn"], "dsort": vf["sort"], @@ -3091,7 +3093,8 @@ class AuthSrv(object): "unlist0": vf.get("unlist") or "", "see_dots": self.args.see_dots, "dqdel": self.args.qdel, - "dgrid": "grid" in vf, + "dlni": vn.js_ls["dlni"], + "dgrid": vn.js_ls["dgrid"], "dgsel": "gsel" in vf, "dnsort": "nsort" in vf, "dhsortn": vf["hsortn"], diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 3c4ab467..588d7bb9 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -30,6 +30,7 @@ def vf_bmap() -> dict[str, str]: } for k in ( "dedup", + "dlni", "dotsrch", "e2d", "e2ds", @@ -318,6 +319,7 @@ flagcats = { "hsortn": "number of sort-rules to add to media URLs", "ufavico=URL": "per-volume favicon (.ico/png/gif/svg)", "unlist": "dont list files matching REGEX", + "dlni": "force-download (no-inline) files on click", "html_head=TXT": "includes TXT in the , or @PATH for file at PATH", "html_head_s=TXT": "additional static text in the html ", "tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)", diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f0f50e5a..17c21532 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -226,6 +226,7 @@ if (1) "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', + "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', "ct_qdel": 'when deleting files, only ask for confirmation once">qdel', @@ -878,6 +879,7 @@ ebi('op_cfg').innerHTML = ( ' ' + L.ct_grid + '\n' + ' Date: Tue, 2 Dec 2025 15:54:38 +0000 Subject: [PATCH 18/67] md-editor: add json beautifier (#794) --- copyparty/web/md.html | 3 ++- copyparty/web/md2.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/copyparty/web/md.html b/copyparty/web/md.html index 1dfed659..f2148a91 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -22,7 +22,8 @@ editor
tools - prettify table (ctrl-k) + beautify table (ctrl-k) + beautify json (ctrl-j) non-ascii: iterate (ctrl-u) non-ascii: markup non-ascii: whitelist diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index f971b742..29243107 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -698,6 +698,39 @@ function reLastIndexOf(txt, ptn, end) { } +// json formatter +function fmt_json(e) { + ev(e); + try { + fmt_json2(); + } + catch (ex) { + return toast.err(7, 'json-format (CTRL-J) failed\n\n(hint: select the json you want to beautify/minify first)\n\n' + ex); + } +} +function fmt_json2() { + var txt = dom_src.value, + o0 = txt.lastIndexOf('\n', dom_src.selectionStart - 1), + o1 = txt.indexOf('\n', dom_src.selectionEnd); + o0 = o0 + 1 ? o0 + 1 : 0; + if (o1 < 0) o1 = txt.length; + for (var a = 0; a < 9; a++) { + if (has(['\r', '\n', ' '], txt.charAt(o0))) ++o0; + if (has(['\r', '\n', ' '], txt.charAt(o1))) --o1; + } + var jt0 = txt.slice(o0, ++o1), + jo = JSON.parse(jt0), + jt = JSON.stringify(jo, null, jt0.indexOf('\n') + 1 ? 0 : 2); + setsel({ + "pre": txt.slice(0, o0), + "sel": jt, + "post": txt.slice(o1), + "car": o0, + "cdr": o0, + }); +} + + // table formatter function fmt_table(e) { ev(e); @@ -997,6 +1030,10 @@ var set_lno = (function () { action_stack.redo(); return false; } + if (kl == "j") { + fmt_json(e.shiftKey); + return false; + } if (kl == "k") { fmt_table(); return false; @@ -1067,6 +1104,7 @@ ebi('help').onclick = function (e) { ebi('fmt_table').onclick = fmt_table; +ebi('fmt_json').onclick = fmt_json; ebi('mark_uni').onclick = mark_uni; ebi('iter_uni').onclick = iter_uni; ebi('cfg_uni').onclick = cfg_uni; From 89cab5b520d3233e857049dc0b046616c65c88c0 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Dec 2025 17:05:21 +0000 Subject: [PATCH 19/67] textfile-viewer: add json-beautifier; closes #794 --- copyparty/web/browser.js | 55 ++++++++++++++++++++++++++++++++-------- copyparty/web/tl/chi.js | 6 ++++- copyparty/web/tl/cze.js | 4 +++ copyparty/web/tl/deu.js | 4 +++ copyparty/web/tl/epo.js | 4 +++ copyparty/web/tl/fin.js | 4 +++ copyparty/web/tl/fra.js | 4 +++ copyparty/web/tl/grc.js | 6 ++++- copyparty/web/tl/ita.js | 4 +++ copyparty/web/tl/kor.js | 4 +++ copyparty/web/tl/nld.js | 4 +++ copyparty/web/tl/nno.js | 3 +++ copyparty/web/tl/nor.js | 3 +++ copyparty/web/tl/pol.js | 4 +++ copyparty/web/tl/por.js | 4 +++ copyparty/web/tl/rus.js | 4 +++ copyparty/web/tl/spa.js | 6 ++++- copyparty/web/tl/swe.js | 4 +++ copyparty/web/tl/tur.js | 4 +++ copyparty/web/tl/ukr.js | 4 +++ scripts/tl.js | 4 +++ 21 files changed, 126 insertions(+), 13 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a03c45bb..39126a4a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -87,6 +87,8 @@ if (1) ["M", "close textfile"], ["E", "edit textfile"], ["S", "select file (for cut/copy/rename)"], + ["Y", "download textfile"], + ["⇧ J", "beautify json"], ] ], @@ -453,6 +455,7 @@ if (1) "tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_next": "show next document$NHotkey: K\">⬇ next", "tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel", + "tvt_j": "beautify json$NHotkey: shift-J\">j", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow", "tvt_wrap": "word-wrap\">↵", @@ -5126,6 +5129,33 @@ var showfile = (function () { return out.join(''); }; + r.ppj = function (e) { + ebi(e); + try { + r.ppj2(); + } + catch (ex) { + toast.err(10, '' + ex); + } + }; + r.ppj2 = function () { + var btn = ebi('dldoc'), + el = ebi('doc'), + t = el.textContent.trim(), + jo = JSON.parse(t), + jt = JSON.stringify(jo, null, t.indexOf('\n') + 1 ? 0 : 2); + el.textContent = jt; + el.innerHTML = '' + el.innerHTML + ''; + try { + el = QS('#doc>code'); + el.className = 'language-json'; + Prism.highlightElement(el); + } + catch (ex) { } + btn.setAttribute('download', ebi('docname').innerHTML); + btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt)); + }; + r.mktree = function () { var top = get_evpath().slice(SR.length), crumbs = linksplit(top).join('/'), @@ -5192,6 +5222,7 @@ var showfile = (function () { '\n' + @@ -5211,6 +5242,7 @@ var showfile = (function () { ebi('prevdoc').onclick = function () { tree_neigh(-1); }; ebi('nextdoc').onclick = function () { tree_neigh(1); }; ebi('seldoc').onclick = r.tglsel; + ebi('ppjdoc').onclick = r.ppj; bcfg_bind(r, 'wrap', 'wrapdoc', true, r.tglwrap); bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail); bcfg_bind(r, 'tail2end', 'tail2end', true); @@ -5885,6 +5917,7 @@ var ahotkeys = function (e) { return; var k = (e.key || e.code) + '', pos = -1, n, + sh = e.shiftKey, ae = document.activeElement, aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; @@ -6007,7 +6040,7 @@ var ahotkeys = function (e) { if (k == '?') return hkhelp(); - if (!e.shiftKey && ctrl(e)) { + if (!sh && ctrl(e)) { var sel = window.getSelection && window.getSelection() || {}; sel = sel && !sel.isCollapsed && sel.direction != 'none'; @@ -6026,7 +6059,16 @@ var ahotkeys = function (e) { return; } - if (e.shiftKey && kl != 'a' && kl != 'd') + if (showfile.active()) { + if (!sh && kl == 's') + return showfile.tglsel() || true; + if (!sh && kl == 'e' && ebi('editdoc').style.display != 'none') + return ebi('editdoc').click() || true; + if (sh && kl == 'j') + return showfile.ppj(e) || true; + } + + if (sh && kl != 'a' && kl != 'd') return; if (/^[0-9]$/.test(k)) @@ -6075,7 +6117,7 @@ var ahotkeys = function (e) { if (k == 'F2') return fileman.rename(); - if (!treectl.hidden && (!e.shiftKey || !thegrid.en)) { + if (!treectl.hidden && (!sh || !thegrid.en)) { if (kl == 'a') return QS('#twig').click(); @@ -6083,13 +6125,6 @@ var ahotkeys = function (e) { return QS('#twobytwo').click(); } - if (showfile.active()) { - if (kl == 's') - showfile.tglsel(); - if (kl == 'e' && ebi('editdoc').style.display != 'none') - ebi('editdoc').click(); - } - if (mp && mp.au && !mp.au.paused) { if (kl == 's') return sel_song(); diff --git a/copyparty/web/tl/chi.js b/copyparty/web/tl/chi.js index 8596c31b..c5822f89 100644 --- a/copyparty/web/tl/chi.js +++ b/copyparty/web/tl/chi.js @@ -83,7 +83,9 @@ Ls.chi = { ["I/K", "前一个/下一个文件"], ["M", "关闭文本文件"], ["E", "编辑文本文件"], - ["S", "选择文件(用于剪切/重命名)"] + ["S", "选择文件(用于剪切/重命名)"], + ["Y", "下载文本文件"], //m + ["⇧ J", "美化json"], //m ] ], @@ -223,6 +225,7 @@ Ls.chi = { "ct_ttips": '◔ ◡ ◔">ℹ️ 工具提示', "ct_thumb": '在网格视图中,切换图标或缩略图$N快捷键: T">🖼️ 缩略图', "ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL', + "ct_dl": '点击文件时强制下载(不内联显示)">dl', //m "ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动', "ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件', "ct_qdel": '删除文件时,只需确认一次">快删', //m @@ -449,6 +452,7 @@ Ls.chi = { "tvt_prev": "显示上一个文档$N快捷键: i\">⬆ 上一个", "tvt_next": "显示下一个文档$N快捷键: K\">⬇ 下一个", "tvt_sel": "选择文件 (用于剪切/删除/...)$N快捷键: S\">选择", + "tvt_j": "美化json$N快捷键: shift-J\">j", //m "tvt_edit": "在文本编辑器中打开文件$N快捷键: E\">✏️ 编辑", "tvt_tail": "监视文件更改,并实时显示新增的行\">📡 跟踪", //m "tvt_wrap": "自动换行\">↵", //m diff --git a/copyparty/web/tl/cze.js b/copyparty/web/tl/cze.js index cfe65526..d789b09f 100644 --- a/copyparty/web/tl/cze.js +++ b/copyparty/web/tl/cze.js @@ -84,6 +84,8 @@ Ls.cze = { ["M", "zavřít textový soubor"], ["E", "upravit textový soubor"], ["S", "vybrat soubor (pro vyjmutí/kopírování/přejmenování)"], + ["Y", "stáhnout textový soubor"], //m + ["⇧ J", "zkrášlit json"], //m ] ], @@ -227,6 +229,7 @@ Ls.cze = { "ct_ttips": '◔ ◡ ◔">ℹ️ nápovědy', "ct_thumb": 'v zobrazení mřížky přepnout ikony nebo náhledy$NKlávesová zkratka: T">🖼️ náhledy', "ct_csel": 'použít CTRL a SHIFT pro výběr souborů v zobrazení mřížky">výběr', + "ct_dl": 'vynutit stažení (nezobrazovat inline) při kliknutí na soubor">dl', //m "ct_ihop": 'když se zavře prohlížeč obrázků, posunout dolů k naposledy zobrazenému souboru">g⮯', "ct_dots": 'zobrazit skryté soubory (pokud to server povoluje)">dotfiles', "ct_qdel": 'při mazání souborů požádat o potvrzení jen jednou">rychlé mazání', @@ -453,6 +456,7 @@ Ls.cze = { "tvt_prev": "zobrazit předchozí dokument$NKlávesová zkratka: i\">⬆ předchozí", "tvt_next": "zobrazit následující dokument$NKlávesová zkratka: K\">⬇ další", "tvt_sel": "vybrat soubor   ( pro vyjmutí / kopírování / mazání / ... )$NKlávesová zkratka: S\">výběr", + "tvt_j": "zkrášlit json$NKlávesová zkratka: shift-J\">j", //m "tvt_edit": "otevřít soubor v textovém editoru$NKlávesová zkratka: E\">✏️ upravit", "tvt_tail": "sledovat soubor pro změny; zobrazit nové řádky v reálném čase\">📡 sledovat", "tvt_wrap": "zalamování slov\">↵", diff --git a/copyparty/web/tl/deu.js b/copyparty/web/tl/deu.js index 4ef76896..df7b87ac 100644 --- a/copyparty/web/tl/deu.js +++ b/copyparty/web/tl/deu.js @@ -84,6 +84,8 @@ Ls.deu = { ["M", "Textdatei schliessen"], ["E", "Textdatei bearbeiten"], ["S", "Textdatei auswählen (für Ausschneiden / Kopieren / Umbenennen)"], + ["Y", "Textdatei herunterladen"], //m + ["⇧ J", "json verschönern"], //m ] ], @@ -223,6 +225,7 @@ Ls.deu = { "ct_ttips": '◔ ◡ ◔">ℹ️ Tooltips', "ct_thumb": 'In Raster-Ansicht, zwischen Icons und Vorschau wechseln$NHotkey: T">🖼️ Vorschaubilder', "ct_csel": 'Benutze STRG und UMSCHALT für Dateiauswahl in Raster-Ansicht">sel', + "ct_dl": 'Herunterladen erzwingen (nicht inline anzeigen), wenn eine datei angeklickt wird">dl', //m "ct_ihop": 'Wenn die Bildanzeige geschlossen ist, scrolle runter zu den zuletzt angesehenen Dateien">g⮯', "ct_dots": 'Verstecke Dateien anzeigen (wenn erlaubt durch Server)">dotfiles', "ct_qdel": 'Nur einmal fragen, wenn mehrere Dateien gelöscht werden">qdel', @@ -449,6 +452,7 @@ Ls.deu = { "tvt_prev": "Vorheriges Dokument zeigen$NHotkey: i\">⬆ vorh.", "tvt_next": "Nächstes Dokument zeigen$NHotkey: K\">⬇ nächst.", "tvt_sel": "Wählt diese Datei aus   ( zum Ausschneiden / Kopieren / Löschen / ... )$NHotkey: S\">ausw.", + "tvt_j": "json verschönern$NHotkey: shift-J\">j", //m "tvt_edit": "Datei im Texteditor zum Bearbeiten öffnen$NHotkey: E\">✏️ bearb.", "tvt_tail": "Datei auf Veränderungen überwachen; Neue Zeilen werden in Echtzeit angezeigt\">📡 folgen", "tvt_wrap": "Zeilenumbruch\">↵", diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index c5a310c5..5f53c39c 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -84,6 +84,8 @@ Ls.epo = { ["M", "fermi dosieron"], ["E", "redakti dosieron"], ["S", "elekti dosieron (por eltondado/kopiado/alinomado)"], + ["Y", "elŝuti tekstodosieron"], //m + ["⇧ J", "beligi json"], //m ] ], @@ -223,6 +225,7 @@ Ls.epo = { "ct_ttips": '◔ ◡ ◔">ℹ️ ŝpruchelpiloj', "ct_thumb": 'dum krado-vido, baskuli montradon de simboloj aŭ bildetoj$NFulmoklavo: T">🖼️ bildetoj', "ct_csel": 'uzi STIR kaj MAJ por elekti dosierojn en krado-vido">elekto', + "ct_dl": 'devigi elŝuton (ne montri enkadre) kiam dosiero estas alklakita">dl', //m "ct_ihop": 'montri la lastan viditan bildo-dosieron post fermado de bildo-vidilo">g⮯', "ct_dots": 'montri kaŝitajn dosierojn (se servilo permesas)">kaŝitaj', "ct_qdel": 'peti konfirmon nur unufoje antaŭ forigado">rapid-forig.', @@ -449,6 +452,7 @@ Ls.epo = { "tvt_prev": "montri malsekvan dokumenton$NFulmoklavo: i\">⬆ malsekva", "tvt_next": "montri sekvan dokumenton$NFulmoklavo: K\">⬇ sekva", "tvt_sel": "elekti dosieron   ( por eltondado / kopiado / forigado / ... )$NFulmoklavo: S\">elekti", + "tvt_j": "beligi json$NFulmoklavo: shift-J\">j", //m "tvt_edit": "malfermi dosieron en teksto-redaktilo$NFulmoklavo: E\">✏️ redakti", "tvt_tail": "observi ŝanĝojn en dosiero; novaj linioj estos tuje montritaj\">📡 gvati", "tvt_wrap": "linifaldo\">↵", diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index cd396221..d8e4fe31 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -84,6 +84,8 @@ Ls.fin = { ["M", "sulje tekstitiedosto"], ["E", "muokkaa tekstitiedostoa"], ["S", "valitse tiedosto (leikkausta/kopiointia/uudelleennimeämistä varten)"], + ["Y", "lataa tekstitiedosto"], //m + ["⇧ J", "kaunista json"], //m ] ], @@ -223,6 +225,7 @@ Ls.fin = { "ct_ttips": '◔ ◡ ◔">ℹ️ vihjelaatikot', "ct_thumb": 'valitse kuvakkeiden / pienoiskuvien välillä kuvanäkymässä $NPikanäppäin: T">🖼️ pienoiskuvat', "ct_csel": 'käytä CTRL ja SHIFT tiedostojen valintaan kuvanäkymässä">valitse', + "ct_dl": 'pakota lataus (älä näytä upotettuna), kun tiedostoa napsautetaan">dl', //m "ct_ihop": 'kun kuvakatselin suljetaan, vieritä alas viimeksi katsottuun tiedostoon">g⮯', "ct_dots": 'näytä piilotetut tiedostot (jos palvelin sallii)">piilotiedostot', "ct_qdel": 'kysy vahvistusta vain kerran tiedostoja poistaessa">qdel', @@ -449,6 +452,7 @@ Ls.fin = { "tvt_prev": "näytä edellinen dokumentti$NPikanäppäin: i\">⬆ edell", "tvt_next": "näytä seuraava dokumentti$NPikanäppäin: K\">⬇ seur", "tvt_sel": "valitse tiedosto   ( leikkausta / kopiointia / poistoa / ... varten )$NPikanäppäin: S\">val", + "tvt_j": "kaunista json$NPikanäppäin: shift-J\">j", //m "tvt_edit": "avaa tiedosto tekstieditorissa$NPikanäppäin: E\">✏️ muokkaa", "tvt_tail": "seuraa tiedoston muutoksia; näytä uudet rivit reaaliaikaisesti\">📡 seuraa", "tvt_wrap": "rivitys\">↵", diff --git a/copyparty/web/tl/fra.js b/copyparty/web/tl/fra.js index 5fabeccb..7731671e 100644 --- a/copyparty/web/tl/fra.js +++ b/copyparty/web/tl/fra.js @@ -84,6 +84,8 @@ Ls.fra = { ["M", "fermer le fichier texte"], ["E", "modifier le fichier texte"], ["S", "sélectioner le fichier (pour le couper/copier/renommer)"], + ["Y", "télécharger le fichier texte"], //m + ["⇧ J", "embellir json"], //m ] ], @@ -223,6 +225,7 @@ Ls.fra = { "ct_ttips": '◔ ◡ ◔">ℹ️ infobulles', "ct_thumb": 'vue en grille, activer les icônes ou les miniatures$NHotkey: T">🖼️ minia', "ct_csel": 'utiliser CTRL et MAJ pour selectioner des fichiers en vue en grille">sel', + "ct_dl": 'forcer le téléchargement (ne pas afficher en ligne) lorsqu’un fichier est cliqué">dl', //m "ct_ihop": 'quand le visionneuse d\'image est fermé, faire defiller vers le bas jusqu\'au dernier fichier">g⮯', "ct_dots": 'voir les fichiers caché (si le serveur le permet)">dotfiles', "ct_qdel": 'ne demander qu\'une confirmation lors de la suppression de fichiers>qdel', @@ -449,6 +452,7 @@ Ls.fra = { "tvt_prev": "montrer le document précédent$NHotkey: i\">⬆ précédent", "tvt_next": "montrer le document suivant$NHotkey: K\">⬇ suivant", "tvt_sel": "sélectionner le fichier   ( pour couper / copier / supprimer / … )$NHotkey: S\">sel", + "tvt_j": "embellir json$NHotkey: shift-J\">j", //m "tvt_edit": "ouvrir le fichier dans l'éditeur de texte$NHotkey: E\">✏️ modifier", "tvt_tail": "surveiller le fichier pour les changements; montrer les nouvelles lignes en temps réel\">📡 suivre", "tvt_wrap": "retour à la ligne\">↵", diff --git a/copyparty/web/tl/grc.js b/copyparty/web/tl/grc.js index 987fc37f..c74ef430 100644 --- a/copyparty/web/tl/grc.js +++ b/copyparty/web/tl/grc.js @@ -83,7 +83,9 @@ Ls.grc = { ["I/K", "προηγούμενο/επόμενο αρχείο"], ["M", "κλείσιμο αρχείου"], ["E", "επεξεργασία αρχείου"], - ["S", "επιλογή αρχείου (για αποκοπή/αντιγραφή/μετονομασία)"] + ["S", "επιλογή αρχείου (για αποκοπή/αντιγραφή/μετονομασία)"], + ["Y", "λήψη αρχείου κειμένου"], //m + ["⇧ J", "ομορφοποίηση json"], //m ] ], @@ -223,6 +225,7 @@ Ls.grc = { "ct_ttips": '◔ ◡ ◔">ℹ️ συμβουλές εργαλείων', "ct_thumb": 'σε προβολή πλέγματος, εναλλαγή εικονιδίων ή μικρογραφιών$NΠλήκτρο συντόμευσης: T">🖼️ μικρογραφίες', "ct_csel": 'χρησιμοποίησε CTRL και SHIFT για επιλογή αρχείων σε προβολή πλέγματος">επιλογή', + "ct_dl": 'εξαναγκασμός λήψης (να μην εμφανίζεται ενσωματωμένα) όταν γίνεται κλικ σε ένα αρχείο">dl', //m "ct_ihop": 'όταν η προβολή εικόνων κλείνει, κάνε scroll στο τελευταίο προβαλλόμενο αρχείο">g⮯', "ct_dots": 'εμφάνιση κρυφών αρχείων (αν το επιτρέπει ο server)">dotfiles', "ct_qdel": 'όταν διαγράφεις αρχεία, ζήτα επιβεβαίωση μόνο μία φορά">γρήγορη διαγραφή', @@ -449,6 +452,7 @@ Ls.grc = { "tvt_prev": "προβολή προηγούμενου εγγράφου$NΣυντόμευση: i\">⬆ προηγούμενο", "tvt_next": "προβολή επόμενου εγγράφου$NΣυντόμευση: K\">⬇ επόμενο", "tvt_sel": "επέλεξε αρχείο   (για αποκοπή / αντιγραφή / διαγραφή / ...)$NΣυντόμευση: S\">επιλογή", + "tvt_j": "ομορφοποίηση json$NΣυντόμευση: shift-J\">j", //m "tvt_edit": "άνοιγμα αρχείου στον επεξεργαστή κειμένου$NΣυντόμευση: E\">✏️ επεξεργασία", "tvt_tail": "παρακολούθηση αρχείου για αλλαγές; εμφάνιση νέων γραμμών σε πραγματικό χρόνο\">📡 παρακολούθηση", "tvt_wrap": "αναδίπλωση λέξεων\">↵", diff --git a/copyparty/web/tl/ita.js b/copyparty/web/tl/ita.js index 63712d5f..3b12eb9e 100644 --- a/copyparty/web/tl/ita.js +++ b/copyparty/web/tl/ita.js @@ -84,6 +84,8 @@ Ls.ita = { ["M", "chiudi file di testo"], ["E", "modifica file di testo"], ["S", "seleziona file (per taglia/copia/rinomina)"], + ["Y", "scarica il file di testo"], //m + ["⇧ J", "abbellire json"], //m ] ], @@ -223,6 +225,7 @@ Ls.ita = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltip', "ct_thumb": 'nella vista griglia, alterna icone o miniature$NTasto rapido: T">🖼️ miniature', "ct_csel": 'usa CTRL e SHIFT per la selezione file nella vista griglia">sel', + "ct_dl": 'forza il download (non visualizzare inline) quando si clicca su un file">dl', //m "ct_ihop": 'quando il visualizzatore immagini è chiuso, scorri fino all\'ultimo file visualizzato">g⮯', "ct_dots": 'mostra file nascosti (se il server lo permette)">dotfile', "ct_qdel": 'quando elimini file, chiedi conferma solo una volta">qdel', @@ -449,6 +452,7 @@ Ls.ita = { "tvt_prev": "mostra documento precedente$NTasto rapido: i\">⬆ prec", "tvt_next": "mostra documento successivo$NTasto rapido: K\">⬇ succ", "tvt_sel": "seleziona file   ( per taglia / copia / elimina / ... )$NTasto rapido: S\">sel", + "tvt_j": "abbellire json$NTasto rapido: shift-J\">j", //m "tvt_edit": "apri file nell'editor di testo$NTasto rapido: E\">✏️ modifica", "tvt_tail": "monitora file per cambiamenti; mostra nuove righe in tempo reale\">📡 segui", "tvt_wrap": "a capo parola\">↵", diff --git a/copyparty/web/tl/kor.js b/copyparty/web/tl/kor.js index 19446466..00249b30 100644 --- a/copyparty/web/tl/kor.js +++ b/copyparty/web/tl/kor.js @@ -84,6 +84,8 @@ Ls.kor = { ["M", "텍스트 파일 닫기"], ["E", "텍스트 파일 편집"], ["S", "파일 선택 (잘라내기/복사/이름 바꾸기용)"], + ["Y", "텍스트 파일 다운로드"], //m + ["⇧ J", "json 미화"], //m ] ], @@ -223,6 +225,7 @@ Ls.kor = { "ct_ttips": '◔ ◡ ◔">ℹ️ 도움말', "ct_thumb": '그리드 보기에서 아이콘 또는 미리보기 이미지 전환$N단축키: T">🖼️ 미리보기', "ct_csel": '그리드 보기에서 CTRL과 SHIFT를 사용하여 파일 선택">선택', + "ct_dl": '파일을 클릭하면 다운로드를 강제로 수행 (인라인으로 표시하지 않음)">dl', //m "ct_ihop": '이미지 뷰어를 닫으면 마지막으로 본 파일로 스크롤">g⮯', "ct_dots": '숨김 파일 표시 (서버가 허용하는 경우)">숨김파일', "ct_qdel": '파일 삭제 시 한 번만 확인 요청">빠른삭제', @@ -449,6 +452,7 @@ Ls.kor = { "tvt_prev": "이전 문서 보기$N단축키: i\">⬆ 이전", "tvt_next": "다음 문서 보기$N단축키: K\">⬇ 다음", "tvt_sel": "파일 선택   (잘라내기/복사/삭제/...용)$N단축키: S\">선택", + "tvt_j": "json 미화$N단축키: shift-J\">j", //m "tvt_edit": "텍스트 편집기에서 파일 열기$N단축키: E\">✏️ 편집", "tvt_tail": "파일 변경 사항 모니터링; 실시간으로 새 줄 표시\">📡 팔로우", "tvt_wrap": "자동 줄 바꿈\">↵", diff --git a/copyparty/web/tl/nld.js b/copyparty/web/tl/nld.js index 870a0991..b21897a4 100644 --- a/copyparty/web/tl/nld.js +++ b/copyparty/web/tl/nld.js @@ -84,6 +84,8 @@ Ls.nld = { ["M", "sluit tekst bestand"], ["E", "bewerk tekst bestand"], ["S", "selecteer bestand (voor knip/kopie/hernoem)"], + ["Y", "tekst bestand downloaden"], //m + ["⇧ J", "json verfraaien"], //m ] ], @@ -223,6 +225,7 @@ Ls.nld = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'In grid-overzicht, wissel tussen iconen of thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'Gebruik CTRL en SHIFT voor de bestand selectie in grid-overzicht>sel', + "ct_dl": 'download afdwingen (niet inline weergeven) wanneer op een bestand wordt geklikt">dl', //m "ct_ihop": 'Als je afbeeldingviewer afsluit, scroll omlaag naar de laatst bekeken bestand">g⮯', "ct_dots": 'Laat verborgen bestanden zien (als de server dat toestaat)">dotfiles', "ct_qdel": 'Waneeer je een bestand verwijderd, vraag eenmalig om bevestiging">qdel', @@ -449,6 +452,7 @@ Ls.nld = { "tvt_prev": "Vorig document tonen$NHotkey: i\">⬆ prev", "tvt_next": "Volgende document tonen$NHotkey: K\">⬇ next", "tvt_sel": "Selecteer bestand   ( voor knip / verplaats / verwijder / ... )$NHotkey: S\">sel", + "tvt_j": "json verfraaien$NHotkey: shift-J\">j", //m "tvt_edit": "Bestand openen in teksteditor$NHotkey: E\">✏️ bewerk", "tvt_tail": "Bestand controleren op wijzigingen; nieuwe regels in realtime weergeven\">📡 volgen", "tvt_wrap": "Automatische terugloop\">↵", diff --git a/copyparty/web/tl/nno.js b/copyparty/web/tl/nno.js index 6966ee90..eceddbc2 100644 --- a/copyparty/web/tl/nno.js +++ b/copyparty/web/tl/nno.js @@ -82,6 +82,7 @@ Ls.nno = { ["E", "redigér tekstdokument"], ["S", "markér fil (for F2/ctrl-x/...)"], ["Y", "last ned tekstfil"], + ["⇧ J", "formattér json"], ] ], @@ -221,6 +222,7 @@ Ls.nno = { "ct_ttips": 'vis hjelpetekst ved å holde musa over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder i staden for ikon$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastane CTRL og SHIFT for markering av filer i ikonvising">merk', + "ct_dl": 'last ned filer (ikkje vis i nettleseren)">dl', "ct_ihop": 'bla ned åt sist viste bilde når bildevisaren lukkast">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillèt det)">.synlig', "ct_qdel": 'sletteknappen spør berre éin gong om stadfesting">hurtig🗑️', @@ -447,6 +449,7 @@ Ls.nno = { "tvt_prev": "vis førre dokument$NSnarvei: i\">⬆ forr.", "tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste", "tvt_sel": "markér fila   ( for utklipp / sletting / ... )$NSnarvei: S\">merk", + "tvt_j": "formattér json$NSnarvei: shift-J\">j", "tvt_edit": "redigér fila$NSnarvei: E\">✏️ endre", "tvt_tail": "overvak fila for endringar og vis nye linjer i sanntid\">📡 følg", "tvt_wrap": "tekstbryting\">↵", diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index 12967339..dddd1dae 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -82,6 +82,7 @@ Ls.nor = { ["E", "rediger tekstdokument"], ["S", "marker fil (for F2/ctrl-x/...)"], ["Y", "last ned tekstfil"], + ["⇧ J", "formattér json"], ] ], @@ -221,6 +222,7 @@ Ls.nor = { "ct_ttips": 'vis hjelpetekst ved å holde musen over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder istedenfor ikoner$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk', + "ct_dl": 'last ned filer (ikke vis i nettleseren)">dl', "ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig', "ct_qdel": 'sletteknappen spør bare én gang om bekreftelse">hurtig🗑️', @@ -447,6 +449,7 @@ Ls.nor = { "tvt_prev": "vis forrige dokument$NSnarvei: i\">⬆ forr.", "tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste", "tvt_sel": "markér filen   ( for utklipp / sletting / ... )$NSnarvei: S\">merk", + "tvt_j": "formattér json$NSnarvei: shift-J\">j", "tvt_edit": "redigér filen$NSnarvei: E\">✏️ endre", "tvt_tail": "overvåk filen for endringer og vis nye linjer i sanntid\">📡 følg", "tvt_wrap": "tekstbryting\">↵", diff --git a/copyparty/web/tl/pol.js b/copyparty/web/tl/pol.js index 86d8a983..73273388 100644 --- a/copyparty/web/tl/pol.js +++ b/copyparty/web/tl/pol.js @@ -84,6 +84,8 @@ Ls.pol = { ["M", "zamknij plik"], ["E", "edytuj plik"], ["S", "wybierz plik (do wycięcia/skopiowania/zmiany nazwy)"], + ["Y", "pobierz plik tekstowy"], //m + ["⇧ J", "upiększ json"], //m ] ], @@ -226,6 +228,7 @@ Ls.pol = { "ct_ttips": '◔ ◡ ◔">ℹ️ podpowiedzi', "ct_thumb": 'w widoku siatki, przełącz ikony i miniaturki$NSkrót: T">🖼️ miniaturki', "ct_csel": 'użyj CTRL i SHIFT do wybierania plików w widoku siatki">wybierz', + "ct_dl": 'wymuś pobieranie (nie wyświetlaj inline) po kliknięciu pliku">dl', //m "ct_ihop": 'przejdź do ostatniego pliku po zamknięciu przeglądarki obrazów">g⮯', "ct_dots": 'pokaż ukryte pliki (jeśli pozwala serwer)">ukryte', "ct_qdel": 'pytaj o potwierdzenie przy usuwaniu tylko raz">pyt. us.', @@ -452,6 +455,7 @@ Ls.pol = { "tvt_prev": "pokaż poprzedni dokument$NSkrót: i\">⬆ poprzedni", "tvt_next": "pokaż następny dokument$NSkrót: K\">⬇ następny", "tvt_sel": "wybierz plik   ( do wycięcia / skopiowania / usunięcia / itp. )$NSkrót: S\">wyb", + "tvt_j": "upiększ json$NSkrót: shift-J\">j", //m "tvt_edit": "otwórz plik w edytorze tekstu$NSkrót: E\">✏️ edytuj", "tvt_tail": "śledź zmiany w pliku; pokazuj nowe linie w czasie rzeczywistym\">📡 śledź", "tvt_wrap": "zawijaj tekst\">↵", diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index 6e2d45b0..8c7def8d 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -84,6 +84,8 @@ Ls.por = { ["M", "fechar arquivo de texto"], ["E", "editar arquivo de texto"], ["S", "selecionar arquivo (para recortar/copiar/renomear)"], + ["Y", "baixar arquivo de texto"], //m + ["⇧ J", "embelezar json"], //m ] ], @@ -223,6 +225,7 @@ Ls.por = { "ct_ttips": '◔ ◡ ◔">ℹ️ dicas de ferramentas', "ct_thumb": 'na visualização de grade, alternar entre ícones ou miniaturas$NHotkey: T">🖼️ miniaturas', "ct_csel": 'usar CTRL e SHIFT para seleção de arquivo na visualização de grade">sel', + "ct_dl": 'forçar download (não exibir inline) ao clicar em um arquivo">dl', //m "ct_ihop": 'quando o visualizador de imagens for fechado, rolar para o último arquivo visualizado">g⮯', "ct_dots": 'mostrar arquivos ocultos (se o servidor permitir)">dotfiles', "ct_qdel": 'ao excluir arquivos, pedir confirmação apenas uma vez">qdel', @@ -449,6 +452,7 @@ Ls.por = { "tvt_prev": "mostrar documento anterior$NHotkey: i\">⬆ anterior", "tvt_next": "mostrar próximo documento$NHotkey: K\">⬇ próximo", "tvt_sel": "selecionar arquivo   ( para recortar / copiar / excluir / ... )$NHotkey: S\">sel", + "tvt_j": "embelezar json$NHotkey: shift-J\">j", //m "tvt_edit": "abrir arquivo no editor de texto$NHotkey: E\">✏️ editar", "tvt_tail": "monitorar arquivo para alterações; mostrar novas linhas em tempo real\">📡 seguir", "tvt_wrap": "quebra de linha\">↵", diff --git a/copyparty/web/tl/rus.js b/copyparty/web/tl/rus.js index 7c7f005b..fa9c9abd 100644 --- a/copyparty/web/tl/rus.js +++ b/copyparty/web/tl/rus.js @@ -84,6 +84,8 @@ Ls.rus = { ["M", "закрыть файл"], ["E", "отредактировать файл"], ["S", "выделить файл"], + ["Y", "скачать текстовый файл"], //m + ["⇧ J", "приукрасить json"], //m ] ], @@ -223,6 +225,7 @@ Ls.rus = { "ct_ttips": '◔ ◡ ◔">ℹ️ подсказки', "ct_thumb": 'переключение между иконками и миниатюрами в режиме сетки$NГорячая клавиша: T">🖼️ миниат.', "ct_csel": 'держите CTRL или SHIFT для выделения файлов в режиме сетки">выбор', + "ct_dl": 'принудительная загрузка (не показывать встроенно) при щелчке по файлу">dl', //m "ct_ihop": 'показывать последний открытый файл после закрытия просмотрщика изображений">g⮯', "ct_dots": 'показывать скрытые файлы (если есть доступ)">скрыт.', "ct_qdel": 'спрашивать подтверждение только один раз перед удалением файлов">быстр. удал.', @@ -449,6 +452,7 @@ Ls.rus = { "tvt_prev": "показать предыдущий документ$NГорячая клавиша: i\">⬆ пред", "tvt_next": "показать следующий документ$NГорячая клавиша: K\">⬇ след", "tvt_sel": "выбрать документ   ( для вырезания / копирования / удаления / ... )$NГорячая клавиша: S\">выд", + "tvt_j": "приукрасить json$NГорячая клавиша: shift-J\">j", //m "tvt_edit": "открыть документ в текстовом редакторе$NГорячая клавиша: E\">✏️ изменить", "tvt_tail": "проверять файл на изменения; показывать новые строки в реальном времени\">📡 обновлять", "tvt_wrap": "перенос слов\">↵", diff --git a/copyparty/web/tl/spa.js b/copyparty/web/tl/spa.js index 27b13163..6c4741bd 100644 --- a/copyparty/web/tl/spa.js +++ b/copyparty/web/tl/spa.js @@ -83,7 +83,9 @@ Ls.spa = { ["I/K", "anterior/siguiente archivo"], ["M", "cerrar archivo"], ["E", "editar archivo"], - ["S", "seleccionar archivo (para cortar/copiar/renombrar)"] + ["S", "seleccionar archivo (para cortar/copiar/renombrar)"], + ["Y", "descargar archivo de texto"], //m + ["⇧ J", "embellecer json"], //m ] ], @@ -222,6 +224,7 @@ Ls.spa = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'en vista de cuadrícula, alternar iconos o miniaturas$NAtajo: T">🖼️ miniaturas', "ct_csel": 'usa CTRL y SHIFT para seleccionar archivos en la vista de cuadrícula">sel', + "ct_dl": 'forzar descarga (no mostrar en línea) al hacer clic en un archivo">dl', //m "ct_ihop": 'al cerrar el visor de imágenes, desplazarse hasta el último archivo visto">g⮯', "ct_dots": 'mostrar archivos ocultos (si el servidor lo permite)">archivos ocultos', "ct_qdel": 'al eliminar archivos, pedir confirmación solo una vez">elim. rápida', @@ -448,6 +451,7 @@ Ls.spa = { "tvt_prev": "mostrar documento anterior$NAtajo: i\">⬆ ant", "tvt_next": "mostrar siguiente documento$NAtajo: K\">⬇ sig", "tvt_sel": "seleccionar archivo   ( para cortar / copiar / eliminar / ... )$NAtajo: S\">sel", + "tvt_j": "embellecer json$NAtajo: shift-J\">j", //m "tvt_edit": "abrir archivo en editor de texto$NAtajo: E\">✏️ editar", "tvt_tail": "monitorizar cambios en el archivo; mostrar nuevas líneas en tiempo real\">📡 seguir", "tvt_wrap": "ajuste de línea\">↵", diff --git a/copyparty/web/tl/swe.js b/copyparty/web/tl/swe.js index 34daf87a..c48e3f5f 100644 --- a/copyparty/web/tl/swe.js +++ b/copyparty/web/tl/swe.js @@ -84,6 +84,8 @@ Ls.swe = { ["M", "stäng textfil"], ["E", "redigera textfil"], ["S", "välj fil"], + ["Y", "ladda ner textfil"], //m + ["⇧ J", "försköna json"], //m ] ], @@ -223,6 +225,7 @@ Ls.swe = { "ct_ttips": '◔ ◡ ◔">ℹ️ tips', "ct_thumb": 'växla mellan miniatyrer och ikoner i rutnätsvyn$NSnabbtangent: T">🖼️ miniatyrer', "ct_csel": 'använd CTRL och SKIFT för urval av filer i rutnätsvyn">val', + "ct_dl": 'tvinga nedladdning (visa inte inline) när en fil klickas">dl', //m "ct_ihop": 'skrolla till den senast visade filen när bildvisaren stängs">g⮯', "ct_dots": 'visa dolda filer (om servern tillåter detta)">dolda', "ct_qdel": 'bekräfta endast en gång när filer raderas">srad', @@ -449,6 +452,7 @@ Ls.swe = { "tvt_prev": "visa föregående fil$NSnabbtangent: i\">⬆ föreg.", "tvt_next": "visa nästa fil$NSnabbtangent: K\">⬇ nästa", "tvt_sel": "välj fil   ( för klipp / kopiera / radera / ... )$NSnabbtangent: S\">välj", + "tvt_j": "försköna json$NSnabbtangent: shift-J\">j", //m "tvt_edit": "öppna fil i textredigerare$NSnabbtangent: E\">✏️ redigera", "tvt_tail": "övervaka filen; visa nya rader i realtid\">📡 övervaka", "tvt_wrap": "automatisk radbrytning\">↵", diff --git a/copyparty/web/tl/tur.js b/copyparty/web/tl/tur.js index fe44438a..677c013b 100644 --- a/copyparty/web/tl/tur.js +++ b/copyparty/web/tl/tur.js @@ -84,6 +84,8 @@ Ls.tur = { ["M", "metin dosyasını kapat"], ["E", "metin dosyasını düzenle"], ["S", "dosyayı seç (kes/kopyala/yeniden adlandır)"], + ["Y", "metin dosyasını indir"], //m + ["⇧ J", "json güzelleştir"], //m ] ], @@ -223,6 +225,7 @@ Ls.tur = { "ct_ttips": '◔ ◡ ◔">ℹ️ ipuçları', "ct_thumb": 'ızgara görünümünde, simgeler ve küçük resimler arasında geçiş yapın$NKısayol: T">🖼️ küçük resimler', "ct_csel": 'ızgara görünümünde dosya seçimi için CTRL ve SHIFT tuşlarını kullanın">seç', + "ct_dl": 'dosyaya tıklandığında indirmeyi zorla (satır içinde görüntüleme)">dl', //m "ct_ihop": 'resim görüntüleyici kapatıldığında, en son görüntülenen dosyaya kaydırın">g⮯', "ct_dots": 'gizli dosyaları göster (sunucu izin veriyorsa)">nokta dosyaları', "ct_qdel": 'dosyaları silerken yalnız bir kez onay isteyin">qdel', @@ -449,6 +452,7 @@ Ls.tur = { "tvt_prev": "önceki belgeyi göster$NKısayol: i\">⬆ önceki", "tvt_next": "sonraki belgeyi göster$NKısayol: K\">⬇ sonraki", "tvt_sel": "dosyayı seç$NKısayol: S\">seç", + "tvt_j": "json güzelleştir$NKısayol: shift-J\">j", //m "tvt_edit": "dosyayı metin düzenleyicisinde aç$NKısayol: E\">✏️ düzenle", "tvt_tail": "dosyalardaki değişiklikleri izle; yeni satırları gerçek zamanlı göster\">📡 takip", "tvt_wrap": "kelime sarma\">↵", diff --git a/copyparty/web/tl/ukr.js b/copyparty/web/tl/ukr.js index f542494a..46805bed 100644 --- a/copyparty/web/tl/ukr.js +++ b/copyparty/web/tl/ukr.js @@ -84,6 +84,8 @@ Ls.ukr = { ["M", "закрити текстовий файл"], ["E", "редагувати текстовий файл"], ["S", "вибрати файл (для вирізання/копіювання/перейменування)"], + ["Y", "завантажити текстовий файл"], //m + ["⇧ J", "прикрасити json"], //m ] ], @@ -223,6 +225,7 @@ Ls.ukr = { "ct_ttips": '◔ ◡ ◔">ℹ️ підказки', "ct_thumb": 'у режимі сітки, перемкнути іконки або мініатюри$NГаряча клавіша: T">🖼️ мініатюри', "ct_csel": 'використовувати CTRL і SHIFT для вибору файлів у режимі сітки">вибір', + "ct_dl": 'примусове завантаження (не показувати вбудовано) під час натискання на файл">dl', //m "ct_ihop": 'коли переглядач зображень закрито, прокрутити вниз до останнього переглянутого файлу">g⮯', "ct_dots": 'показати приховані файли (якщо сервер дозволяє)">приховані файли', "ct_qdel": 'при видаленні файлів, запитати підтвердження лише один раз">швидке видалення', @@ -449,6 +452,7 @@ Ls.ukr = { "tvt_prev": "показати попередній документ$NГаряча клавіша: i\">⬆ попер", "tvt_next": "показати наступний документ$NГаряча клавіша: K\">⬇ наст", "tvt_sel": "вибрати файл   ( для вирізання / копіювання / видалення / ... )$NГаряча клавіша: S\">вибр", + "tvt_j": "прикрасити json$NГаряча клавіша: shift-J\">j", //m "tvt_edit": "відкрити файл в текстовому редакторі$NГаряча клавіша: E\">✏️ редагувати", "tvt_tail": "моніторити файл на зміни; показувати нові рядки в реальному часі\">📡 слідкувати", "tvt_wrap": "перенесення слів\">↵", diff --git a/scripts/tl.js b/scripts/tl.js index 7c9010f7..a8c8ea74 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -113,6 +113,8 @@ Ls.hmn = { ["M", "close textfile"], ["E", "edit textfile"], ["S", "select file (for cut/copy/rename)"], + ["Y", "download textfile"], + ["⇧ J", "beautify json"], ] ], @@ -252,6 +254,7 @@ Ls.hmn = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', + "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', "ct_qdel": 'when deleting files, only ask for confirmation once">qdel', @@ -478,6 +481,7 @@ Ls.hmn = { "tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_next": "show next document$NHotkey: K\">⬇ next", "tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel", + "tvt_j": "beautify json$NHotkey: shift-J\">j", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow", "tvt_wrap": "word-wrap\">↵", From d9f76882e735190f773fecfeeb8910ef3274afed Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Dec 2025 19:20:04 +0000 Subject: [PATCH 20/67] md-edit: fix sbs in ff52/chrome49 --- copyparty/web/md2.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/web/md2.css b/copyparty/web/md2.css index ec4a7b29..6673118a 100644 --- a/copyparty/web/md2.css +++ b/copyparty/web/md2.css @@ -9,10 +9,13 @@ width: calc(100% - 56em); } #mw { - left: max(0em, calc(100% - 55em)); overflow-y: auto; position: fixed; bottom: 0; + left: 0; +} +@media (min-width: 55em) { + #mw {left:calc(100% - 55em)} } From b314e30db837930172d834e6823237dce5623b78 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Dec 2025 19:45:08 +0000 Subject: [PATCH 21/67] readme: add server hall of fame --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index adcba825..2dd247e5 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ made in Norway 🇳🇴 * [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nixos module](#nixos-module) * [browser support](#browser-support) - TLDR: yes +* [server hall of fame](#server-hall-of-fame) - unexpected things that run copyparty * [client examples](#client-examples) - interact with copyparty using non-browser clients * [folder sync](#folder-sync) - sync folders to/from copyparty * [mount as drive](#mount-as-drive) - a remote copyparty server as a local filesystem @@ -2623,6 +2624,15 @@ quick summary of more eccentric web-browsers trying to view a directory index:

+# server hall of fame + +unexpected things that run copyparty: + +* an old [allwinner](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg) android tv-box (ziptie-strapped to an HDD) running a firmware which flips the CPU into Big-Endian mode early during boot + * copyparty is [certified BE ready](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png) +* a [wristwatch](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/clockyparty.jpg) + + # client examples interact with copyparty using non-browser clients From cdffde7813bd571e2d5cde0886e8837a70108efd Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Dec 2025 20:47:01 +0000 Subject: [PATCH 22/67] v1.19.21 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 14 ++++++++++++++ scripts/pyinstaller/deps.sha512 | 4 ++-- scripts/pyinstaller/notes.txt | 4 ++-- tests/util.py | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index fe38c3e4..0272b761 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 20) +VERSION = (1, 19, 21) CODENAME = "usernames" -BUILD_DT = (2025, 11, 2) +BUILD_DT = (2025, 12, 2) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index 2b5a8b95..65366fa1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,17 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-1102-0109 `v1.19.20` november + +## 🧪 new features + +* #961 the `/?shares` listing now shows the list of filenames for each share 2cc53ea15181f750b4367e6cd20dfebd0bcb3bee + +## 🩹 bugfixes + +* #967 per-volume md/lg sandbox rules are now applied during navigation db60951d9fa5b17c8190e8b3ab4ceb422a9d2701 + * if a volume has `no-sb-lg` or `no-sb-md` set then it'll apply when navigating into that volume, and vice-versa + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-1025-1918 `v1.19.19` copyparty.eu マークII diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index 3ec760f9..13180cce 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -28,8 +28,8 @@ f4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa22 00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl 8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl -3e39ea6e16b502d99a2e6544579095d0f7c6097761cd85135d5e929b9dec1b32e80669a846f94ee8c2cca9be2f5fe728625d09453988864c04e16bb8445c3f91 pillow-11.3.0-cp313-cp313-win_amd64.whl +fa5d24c51e39760fc5121e56e9948384e03f62b66907ba313a6a803dd601832df62fb5066f3019620664d7cc6b0482e13000cd2d3d1553b709a56a347919565e pillow-12.0.0-cp313-cp313-win_amd64.whl b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl -419f499560f09b770060ef336926f5bf2776b5c33937969ce75d1e3263735de1ed6eb2199ae88797cba0e4cb17de4a235beec4d7985f993ecb3de7320c482917 python-3.13.9-amd64.exe +7d937df1345407398f215c0943514f9dadf2a951b2687e20c06116b2cb3e1d641289da705fcc0b3548f77d19f42f52306c27aa1fc00ed56d19bf47e839c495a5 python-3.13.10-amd64.exe 2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 92ab8748..936c9f67 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -39,10 +39,10 @@ fns=( MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl mutagen-1.47.0-py3-none-any.whl packaging-25.0-py3-none-any.whl - pillow-11.3.0-cp313-cp313-win_amd64.whl + pillow-12.0.0-cp313-cp313-win_amd64.whl pyinstaller-6.15.0-py3-none-win_amd64.whl pyinstaller_hooks_contrib-2025.8-py3-none-any.whl - python-3.13.9-amd64.exe + python-3.13.10-amd64.exe setuptools-80.9.0-py3-none-any.whl ) [ $w7 ] && fns+=( diff --git a/tests/util.py b/tests/util.py index 0ab2a0e4..8ba5e8c4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt dlni e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip" From 29925dc22b1f6810768d0af25d9c35ee35d88aa7 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Dec 2025 20:51:10 +0000 Subject: [PATCH 23/67] update pkgs to 1.19.21 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/makedeb-mpr/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 7cf8e45c..6929aa77 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -3,7 +3,7 @@ # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty -pkgver="1.19.20" +pkgver="1.19.21" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) -sha256sums=("050ccc34554e59210aca7a67d87a186e69b3f4dbe013d5ee2f11a22c259a82a6") +sha256sums=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index b7dfdb66..37cf79a8 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.20 +pkgver=1.19.21 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) -sha256sums=("050ccc34554e59210aca7a67d87a186e69b3f4dbe013d5ee2f11a22c259a82a6") +sha256sums=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 0dd8a1bb..361019f1 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.19.20/copyparty-1.19.20.tar.gz", - "version": "1.19.20", - "hash": "sha256-BQzMNFVOWSEKynpn2HoYbmmz9NvgE9XuLxGiLCWagqY=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.21/copyparty-1.19.21.tar.gz", + "version": "1.19.21", + "hash": "sha256-RHI6gj8hjlKq7GB1aVlzp1uGY8kgLID9c/SOUsYazUI=" } \ No newline at end of file From fa918228d56e1384e77adc193c4bd26356f6bd18 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 4 Dec 2025 17:50:17 +0000 Subject: [PATCH 24/67] wram: also prevent moves in addition to write-perms, also drop move-perms from ramdisks since that is another potential source for confusion additionally, write-access was correctly prevented, but the ui would still indicate write permission, so fix that too --- copyparty/fsutil.py | 6 ++++++ copyparty/web/browser.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/copyparty/fsutil.py b/copyparty/fsutil.py index 70421e8f..d473c016 100644 --- a/copyparty/fsutil.py +++ b/copyparty/fsutil.py @@ -211,6 +211,12 @@ def ramdisk_chk(asrv: AuthSrv) -> None: if fs == "tmpfs" or (mp == "/" and fs in ramfs): mods.append((vn.vpath, ap, fs, mp)) vn.axs.uwrite.clear() + vn.axs.umove.clear() + for un, ztsp in list(vn.uaxs.items()): + zsl = list(ztsp) + zsl[1] = False + zsl[2] = False + vn.uaxs[un] = zsl if mods: t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:" t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods] diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index b467af72..b7bd214f 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -53,7 +53,7 @@ 📝 -

+
From c5c5f9b4b828b984cf7109d12f86150a334eb566 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 4 Dec 2025 23:57:54 +0000 Subject: [PATCH 25/67] readme: add help links --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2dd247e5..656c29d2 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ some recommended options: * `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen) * `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar` * replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else - * see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions + * see [accounts and volumes](#accounts-and-volumes) (or [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)) for the syntax and other permissions ### mirrors @@ -498,7 +498,7 @@ per-folder, per-user permissions - if your setup is getting complex, consider m * much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume) * changes to the `[global]` config section requires a restart to take effect -a quick summary can be seen using `--help-accounts` +a quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page) configuring accounts/volumes with arguments: * `-a usr:pwd` adds account `usr` with password `pwd` @@ -1263,7 +1263,7 @@ see [./srv/expand/](./srv/expand/) for usage and examples * and `PREADME.md` / `preadme.md` is shown above directory listings unless `--no-readme` or `.prologue.html` -* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see `--help-exp` +* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see [`--help-exp`](https://copyparty.eu/cli/#exp-help-page) ## searching @@ -1293,10 +1293,9 @@ using arguments or config files, or a mix of both: * or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume * changes to the `[global]` config section requires a restart to take effect -**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags. +**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with [`--help`](https://copyparty.eu/cli/) (or click that link) to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in [`--help-flags`](https://copyparty.eu/cli/#flags-help-page) can be used in volumes as volflags. * if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help` -* or see this: https://ocv.me/copyparty/helptext.html -* or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt +* or if you prefer plaintext, https://copyparty.eu/helptext.txt ## zeroconf @@ -1889,7 +1888,7 @@ trigger a program on uploads, renames etc ([examples](./bin/hooks/)) 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` +there's a bunch of flags and stuff, see [`--help-hooks`](https://copyparty.eu/cli/#hooks-help-page) if you want to write your own hooks, see [devnotes](./docs/devnotes.md#event-hooks) @@ -2820,7 +2819,7 @@ some notes on hardening * cors doesn't work right otherwise * if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml` * this returns html documents as plaintext, and also disables markdown rendering -* when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or `--help-bind` +* when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or [`--help-bind`](https://copyparty.eu/cli/#bind-help-page) safety profiles: @@ -2908,7 +2907,7 @@ dirkeys are generated based on another salt (`--dk-salt`) + filesystem-path and ## password hashing -you can hash passwords before putting them into config files / providing them as arguments; see `--help-pwhash` for all the details +you can hash passwords before putting them into config files / providing them as arguments; see [`--help-pwhash`](https://copyparty.eu/cli/#pwhash-help-page) for all the details `--ah-alg argon2` enables it, and if you have any plaintext passwords then it'll print the hashed versions on startup so you can replace them From a86983928c601fe1050e31797ad85601e8a294a0 Mon Sep 17 00:00:00 2001 From: emilia <46273791+emiliatheworst@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:46:14 -0500 Subject: [PATCH 26/67] fix podman-systemd typo; closes #1088 (#988) Signed-off-by: emilia <46273791+emiliatheworst@users.noreply.github.com> --- contrib/podman-systemd/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/podman-systemd/README.md b/contrib/podman-systemd/README.md index 09e2c19c..02d1a73c 100644 --- a/contrib/podman-systemd/README.md +++ b/contrib/podman-systemd/README.md @@ -24,8 +24,8 @@ Note that you can select the owner and group of this volume by changing the `uid To install and start copyparty with Podman and systemd as the root user, run the following: ```shell -sudo mkdir -pv /etc/systemd/container/ /etc/copyparty/ -sudo cp -v copyparty.container /etc/systemd/containers/ +sudo mkdir -pv /etc/containers/systemd/ /etc/copyparty/ +sudo cp -v copyparty.container /etc/containers/systemd/ sudo cp -v copyparty.conf /etc/copyparty/ sudo systemctl daemon-reload sudo systemctl start copyparty From 1b0eb450325bca78c6683ee3a0f68591accbd18e Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 11 Dec 2025 16:26:48 +0000 Subject: [PATCH 27/67] synology: hide `@eaDir` folders everywhere --- docs/synology-dsm.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/synology-dsm.md b/docs/synology-dsm.md index b9388b53..60e6adcf 100644 --- a/docs/synology-dsm.md +++ b/docs/synology-dsm.md @@ -40,6 +40,7 @@ open the Package Center and install `Text Editor` (by Synology Inc.) to create a rss, daw, ver # some other nice-to-have features #dedup # you may want this, or maybe not hist: /cfg/hist # don't pollute the shared-folder + unlist: ^@eaDir # hide the synology "@eaDir" folders name: synology # shows in the browser, can be anything [accounts] @@ -49,10 +50,6 @@ open the Package Center and install `Text Editor` (by Synology Inc.) to create a /w # the "/w" docker-volume (the shared-folder) accs: A: ed # give Admin to username ed - -# hide the synology system files by creating a hidden volume -[/@eaDir] - /w/@eaDir ``` if you ever change the copyparty config file, then [restart the container](https://ocv.me/copyparty/doc/pics/dsm71-02.png) to make the changes take effect From 7d526eaba32d953e2dfbb3931f01cddc6109c1f3 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 11 Dec 2025 16:41:55 +0000 Subject: [PATCH 28/67] fix termsz on windows --- bin/u2c.py | 10 ++++++++-- copyparty/util.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index 6a6f3156..16173cb4 100755 --- a/bin/u2c.py +++ b/bin/u2c.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from __future__ import print_function, unicode_literals -S_VERSION = "2.15" -S_BUILD_DT = "2025-10-25" +S_VERSION = "2.16" +S_BUILD_DT = "2025-12-11" """ u2c.py: upload to copyparty @@ -492,6 +492,12 @@ print = safe_print if VT100 else flushing_print def termsize(): + try: + w, h = os.get_terminal_size() + return w, h + except: + pass + env = os.environ def ioctl_GWINSZ(fd): diff --git a/copyparty/util.py b/copyparty/util.py index 7290dd9e..b16dd548 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -4175,7 +4175,12 @@ def wrap(txt: str, maxlen: int, maxlen2: int) -> list[str]: def termsize() -> tuple[int, int]: - # from hashwalk + try: + w, h = os.get_terminal_size() + return w, h + except: + pass + env = os.environ def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]: From ad45de94410c6eb6259781b35445e71717e07dc0 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 11 Dec 2025 21:32:43 +0000 Subject: [PATCH 29/67] enforce x-forwarded-host when reverse-proxied; if x-forwarded-for is present, then also require x-forwarded-host and x-forwarded-proto avoids displaying subtly-incorrect values on the connect-page and instead shows blatantly-incorrect values ("example.com") the headernames x-forwarded-host and x-forwarded-proto can be configured with global-options xf-host and xf-proto --- copyparty/__main__.py | 2 ++ copyparty/httpcli.py | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 7b5c32a1..3ed24a14 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1275,6 +1275,8 @@ def add_network(ap): ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=9999999, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m-1\033[0m]=closest-proxy, [\033[32m-2\033[0m]=second-hop, [\033[32m-3\033[0m]=third-hop") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") + ap2.add_argument("--xf-host", metavar="NAME", type=u, default="x-forwarded-host", help="if reverse-proxied, which http header to read the correct Host value from; this header must contain the server's external domain name") + ap2.add_argument("--xf-proto", metavar="NAME", type=u, default="x-forwarded-proto", help="if reverse-proxied, which http header to read the correct protocol value from; this header must contain either 'http' or 'https'") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 395e7254..7b3fb67b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -150,7 +150,8 @@ NO_CACHE = {"Cache-Control": "no-cache"} ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() -BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" +BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy, or --xf-host is incorrect)" +BADXFF2 = ". Some copyparty features are now disabled as a safety measure." H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -221,12 +222,11 @@ class HttpCli(object): self.log_func = conn.log_func # mypy404 self.log_src = conn.log_src # mypy404 self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey - self.tls: bool = hasattr(self.s, "cipher") + self.tls = self.is_https = hasattr(self.s, "cipher") self.is_vproxied = bool(self.args.R) # placeholders; assigned by run() self.keepalive = False - self.is_https = False self.in_hdr_recv = True self.headers: dict[str, str] = {} self.mode = " " # http verb @@ -390,9 +390,6 @@ class HttpCli(object): self.keepalive = "close" not in zs and ( self.http_ver != "HTTP/1.0" or zs == "keep-alive" ) - self.is_https = ( - self.headers.get("x-forwarded-proto", "").lower() == "https" or self.tls - ) self.host = self.headers.get("host") or "" if not self.host: if self.s.family == socket.AF_UNIX: @@ -417,7 +414,7 @@ class HttpCli(object): self.bad_xff = True if self.args.rproxy != 9999999: t = "global-option --rproxy %d could not be used (out-of-bounds) for the received header [%s]" - self.log(t % (self.args.rproxy, zso), c=3) + self.log(t % (self.args.rproxy, zso) + BADXFF2, c=3) else: zsl = [ " rproxy: %d if this client's IP-address is [%s]" @@ -436,6 +433,7 @@ class HttpCli(object): t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"' else: t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"' + t += BADXFF2 if "." in pip: zs = ".".join(pip.split(".")[:2]) + ".0.0/16" @@ -448,7 +446,23 @@ class HttpCli(object): else: self.ip = cli_ip self.log_src = self.conn.set_rproxy(self.ip) - self.host = self.headers.get("x-forwarded-host") or self.host + try: + self.host = self.headers[self.args.xf_host] + self.is_https = len(self.headers[self.args.xf_proto]) == 5 + except: + self.bad_xff = True + if self.args.xf_host not in self.headers: + self.host = "example.com" + t = 'got proxied request without header "%s" (global-option "xf-host"). This header must contain the true external "Host" value (the domain-name of the website). Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-host" to another header-name to read this value from' + self.log(t % (self.args.xf_host,) + BADXFF2, 3) + if self.args.xf_proto not in self.headers: + t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-proto" to another header-name to read this value from' + self.log(t % (self.args.xf_proto,) + BADXFF2, 3) + + # the semantics of trusted_xff and bad_xff are different; + # trusted_xff is whether the connection came from a trusted reverseproxy, + # regardless of whether the client ip detection is correctly configured + # (the primary safeguard for idp is --idp-h-key) trusted_xff = True m = RE_HOST.search(self.host) @@ -5717,17 +5731,18 @@ class HttpCli(object): and (self.uname in vol.axs.uread or self.uname in vol.axs.upget) } - bad_xff = hasattr(self, "bad_xff") - if bad_xff: + if hasattr(self, "bad_xff"): allvols = [] t = "will not return list of recent uploads" + BADXFF self.log(t, 1) if self.avol: raise Pebkac(500, t) - x = self.conn.hsrv.broker.ask( - "up2k.get_unfinished_by_user", self.uname, "" if bad_xff else self.ip - ) + x = self.conn.hsrv.broker.ask("up2k.get_unfinished_by_user", self.uname, "") + else: + x = self.conn.hsrv.broker.ask( + "up2k.get_unfinished_by_user", self.uname, self.ip + ) zdsa: dict[str, Any] = x.get() uret: list[dict[str, Any]] = [] if "timeout" in zdsa: From ce2eeba22694f2a6a3d31ea382b8d206d979266c Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 11 Dec 2025 21:38:36 +0000 Subject: [PATCH 30/67] custom ban-message --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 7 +++++-- copyparty/svchub.py | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3ed24a14..af282f0c 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1557,6 +1557,7 @@ def add_safety(ap): ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--dont-ban", metavar="TXT", type=u, default="no", help="anyone at this accesslevel or above will not get banned: [\033[32mav\033[0m]=admin-in-volume, [\033[32maa\033[0m]=has-admin-anywhere, [\033[32mrw\033[0m]=read-write, [\033[32mauth\033[0m]=authenticated, [\033[32many\033[0m]=disable-all-bans, [\033[32mno\033[0m]=anyone-can-get-banned") + ap2.add_argument("--banmsg", metavar="TXT", type=u, default="thank you for playing \u00a0 (see serverlog and readme)", help="the response to send to banned users; can be @ban.html to send the contents of ban.html") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 7b3fb67b..739e7ced 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -969,7 +969,7 @@ class HttpCli(object): return False self.log("banned for {:.0f} sec".format(rt), 6) - self.terse_reply(b"thank you for playing (see serverlog and readme)", 403) + self.terse_reply(self.args.banmsg_b, 403) return True def permit_caching(self) -> None: @@ -1152,7 +1152,10 @@ class HttpCli(object): ] if body: - lines.append("Content-Length: " + unicode(len(body))) + lines.append( + "Content-Type: text/html; charset=utf-8\r\nContent-Length: " + + unicode(len(body)) + ) lines.append("\r\n") self.s.sendall("\r\n".join(lines).encode("utf-8") + body) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 9c954528..a414d750 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1102,6 +1102,12 @@ class SvcHub(object): else: setattr(al, k, re.compile("^" + vs + "$")) + if al.banmsg.startswith("@"): + with open(al.banmsg[1:], "rb") as f: + al.banmsg_b = f.read() + else: + al.banmsg_b = al.banmsg.encode("utf-8") + b"\n" + if not al.sus_urls: al.ban_url = "no" elif al.ban_url == "no": From 1b222fb5763f27808e01be1aa1026ada1ee34f3d Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 11 Dec 2025 22:15:46 +0000 Subject: [PATCH 31/67] revert to `X-Forwarded-Host` being optional; turns out reverseproxies keeping the initial Host value is the far more common case; requiring X-Forwarded-Host is a bad idea partially reverts ad45de94410c6eb6259781b35445e71717e07dc0 --- copyparty/httpcli.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 739e7ced..6f7a57b1 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -150,7 +150,7 @@ NO_CACHE = {"Cache-Control": "no-cache"} ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() -BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy, or --xf-host is incorrect)" +BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF2 = ". Some copyparty features are now disabled as a safety measure." H_CONN_KEEPALIVE = "Connection: Keep-Alive" @@ -446,18 +446,14 @@ class HttpCli(object): else: self.ip = cli_ip self.log_src = self.conn.set_rproxy(self.ip) + self.host = self.headers.get(self.args.xf_host, self.host) try: - self.host = self.headers[self.args.xf_host] self.is_https = len(self.headers[self.args.xf_proto]) == 5 except: self.bad_xff = True - if self.args.xf_host not in self.headers: - self.host = "example.com" - t = 'got proxied request without header "%s" (global-option "xf-host"). This header must contain the true external "Host" value (the domain-name of the website). Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-host" to another header-name to read this value from' - self.log(t % (self.args.xf_host,) + BADXFF2, 3) - if self.args.xf_proto not in self.headers: - t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-proto" to another header-name to read this value from' - self.log(t % (self.args.xf_proto,) + BADXFF2, 3) + self.host = "example.com" + t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-proto" to another header-name to read this value from' + self.log(t % (self.args.xf_proto,) + BADXFF2, 3) # the semantics of trusted_xff and bad_xff are different; # trusted_xff is whether the connection came from a trusted reverseproxy, From a1cbac02520aca83491d22ced3e5dd6979abf3cd Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Dec 2025 07:51:01 +0000 Subject: [PATCH 32/67] option to set thumbnail quality (#1092); plus these fixes: * adds a previously missed libvips optimization, giving much smaller files at the same quality * try to align the quality-scale of each backend (pillow, libvips, ffmpeg) by filesize --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 2 +- copyparty/cfg.py | 2 ++ copyparty/th_srv.py | 80 +++++++++++++++++++++++++++++++++++++------ tests/util.py | 2 +- 5 files changed, 75 insertions(+), 12 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index af282f0c..137bfd07 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1646,6 +1646,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") + ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index de6dc18b..1fd5af99 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2384,7 +2384,7 @@ class AuthSrv(object): if vf not in vol.flags: vol.flags[vf] = getattr(self.args, ga) - zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who" + zs = "forget_ip gid nrand tail_who th_qv th_spec_p u2abort u2ow uid unp_who ups_who zip_who" for k in zs.split(): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 588d7bb9..799ab9df 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -133,6 +133,7 @@ def vf_vmap() -> dict[str, str]: "tail_tmax", "tail_who", "tcolor", + "th_qv", "th_spec_p", "txt_eol", "unlist", @@ -289,6 +290,7 @@ flagcats = { "thsize": "thumbnail res; WxH", "crop": "center-cropping (y/n/fy/fn)", "th3x": "3x resolution (y/n/fy/fn)", + "th_qv=40": "thumbnail quality (10~90)", "convt": "convert-to-image timeout in seconds", "aconvt": "convert-to-audio timeout in seconds", "th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always", diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 684367c5..4aa5746c 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -14,7 +14,7 @@ import time from queue import Queue -from .__init__ import ANYWIN, PY2, TYPE_CHECKING +from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .authsrv import VFS from .bos import bos from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe @@ -56,6 +56,56 @@ EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split()) PTN_TS = re.compile("^-?[0-9a-f]{8,10}$") +# for n in {1..100}; do rm -rf /home/ed/Pictures/wp/.hist/th/ ; python3 -m copyparty -qv /home/ed/Pictures/wp/::r --th-no-webp --th-qv $n --th-dec pil >/dev/null 2>&1 & p=$!; printf '\033[A\033[J%3d ' $n; while true; do sleep 0.1; curl -s 127.1:3923 >/dev/null && break; done; curl -s '127.1:3923/?tar=j' >/dev/null ; cat /home/ed/Pictures/wp/.hist/th/1n/bs/1nBsjDetfie1iDq3y2D4YzF5/*.* | wc -c; kill $p; wait >/dev/null 2>&1; done +# filesize-equivalent, not quality (ff looks much shittier) +FF_JPG_Q = { + 0: b"30", # 0 + 1: b"30", # 5 + 2: b"30", # 10 + 3: b"30", # 15 + 4: b"28", # 20 + 5: b"21", # 25 + 6: b"17", # 30 + 7: b"15", # 35 + 8: b"13", # 40 + 9: b"12", # 45 + 10: b"11", # 50 + 11: b"10", # 55 + 12: b"9", # 60 + 13: b"8", # 65 + 14: b"7", # 70 + 15: b"6", # 75 + 16: b"5", # 80 + 17: b"4", # 85 + 18: b"3", # 90 + 19: b"2", # 95 + 20: b"2", # 100 +} +# FF_JPG_Q = {xn: ("%d" % (xn,)).encode("ascii") for xn in range(2, 33)} +VIPS_JPG_Q = { + 0: 4, # 0 + 1: 7, # 5 + 2: 12, # 10 + 3: 17, # 15 + 4: 22, # 20 + 5: 27, # 25 + 6: 32, # 30 + 7: 37, # 35 + 8: 42, # 40 + 9: 47, # 45 + 10: 52, # 50 + 11: 56, # 55 + 12: 61, # 60 + 13: 66, # 65 + 14: 71, # 70 + 15: 75, # 75 + 16: 80, # 80 + 17: 85, # 85 + 18: 89, # 90 (vips explodes past this point) + 19: 91, # 95 + 20: 97, # 100 +} + try: if os.environ.get("PRTY_NO_PIL"): @@ -529,7 +579,7 @@ class ThumbSrv(object): im.thumbnail(self.getres(vn, fmt)) fmts = ["RGB", "L"] - args = {"quality": 40} + args = {"quality": vn.flags["th_qv"]} if tpath.endswith(".webp"): # quality 80 = pillow-default @@ -573,7 +623,12 @@ class ThumbSrv(object): raise assert img # type: ignore # !rm - img.write_to_file(tpath, Q=40) + args = {} + qv = vn.flags["th_qv"] + if tpath.endswith("jpg"): + qv = VIPS_JPG_Q[qv // 5] + args["optimize_coding"] = True + img.write_to_file(tpath, Q=qv, strip=True, **args) def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) @@ -607,7 +662,12 @@ class ThumbSrv(object): raise assert img # type: ignore # !rm - img.write_to_file(tpath, Q=40) + args = {} + qv = vn.flags["th_qv"] + if tpath.endswith("jpg"): + qv = VIPS_JPG_Q[qv // 5] + args["optimize_coding"] = True + img.write_to_file(tpath, Q=qv, strip=True, **args) elif HAVE_PIL: if thumb.format == rawpy.ThumbFormat.BITMAP: im = Image.fromarray(thumb.data, "RGB") @@ -671,12 +731,12 @@ class ThumbSrv(object): if tpath.endswith(".jpg"): cmd += [ b"-q:v", - b"6", # default=?? + FF_JPG_Q[vn.flags["th_qv"] // 5], # default=?? ] else: cmd += [ b"-q:v", - b"50", # default=75 + unicode(vn.flags["th_qv"]).encode("ascii"), # default=75 b"-compression_level:v", b"6", # default=4, 0=fast, 6=max ] @@ -722,7 +782,7 @@ class ThumbSrv(object): if len(lines) > 50: lines = lines[:25] + ["[...]"] + lines[-25:] - txt = "\n".join(["ff: " + str(x) for x in lines]) + txt = "\n".join(["ff: " + unicode(x) for x in lines]) if len(txt) > 5000: txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:] @@ -880,12 +940,12 @@ class ThumbSrv(object): if tpath.endswith(".jpg"): cmd += [ b"-q:v", - b"6", # default=?? + FF_JPG_Q[vn.flags["th_qv"] // 5], # default=?? ] else: cmd += [ b"-q:v", - b"50", # default=75 + unicode(vn.flags["th_qv"]).encode("ascii"), # default=75 b"-compression_level:v", b"6", # default=4, 0=fast, 6=max ] @@ -1143,7 +1203,7 @@ class ThumbSrv(object): ret = [] for k, vs in raw_tags.items(): for v in vs: - if len(str(v)) >= 1024: + if len(unicode(v)) >= 1024: bv = k.encode("utf-8", "replace") ret += [b"-metadata", bv + b"="] break diff --git a/tests/util.py b/tests/util.py index 8ba5e8c4..91be4e0b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -158,7 +158,7 @@ class Cfg(Namespace): ex = "hash_mt hsortn qdel safe_dedup scan_pr_r scan_pr_s scan_st_r srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who" ka.update(**{k: 1 for k in ex.split()}) - ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who ver_iwho zip_who" + ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv ups_who ver_iwho zip_who" ka.update(**{k: 9 for k in ex.split()}) ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" From ca6c4deaac15b81240f26b0f02b99b38587e953e Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Dec 2025 21:25:33 +0000 Subject: [PATCH 33/67] delete thumbnail-cache if settings change --- copyparty/th_srv.py | 66 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 4aa5746c..84906dfc 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -358,6 +358,7 @@ class ThumbSrv(object): if not bos.path.exists(inf_path): with open(inf_path, "wb") as f: f.write(afsenc(os.path.dirname(abspath))) + self.writevolcfg(histpath) self.busy[tpath] = [cond] do_conv = True @@ -401,6 +402,47 @@ class ThumbSrv(object): "ffa": self.fmt_ffa, } + def volcfgi(self, vn: VFS) -> str: + ret = [] + zs = "th_dec th_no_webp th_no_jpg" + for zs in zs.split(" "): + ret.append("%s(%s)\n" % (zs, getattr(self.args, zs))) + zs = "th_qv thsize th_spec_p convt" + for zs in zs.split(" "): + ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) + return "".join(ret) + + def volcfga(self, vn: VFS) -> str: + ret = [] + zs = "q_opus q_mp3" + for zs in zs.split(" "): + ret.append("%s(%s)\n" % (zs, getattr(self.args, zs))) + zs = "aconvt" + for zs in zs.split(" "): + ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) + return "".join(ret) + + def writevolcfg(self, histpath: str) -> None: + try: + bos.stat(os.path.join(histpath, "th", "cfg.txt")) + bos.stat(os.path.join(histpath, "ac", "cfg.txt")) + return + except: + pass + cfgi = cfga = "" + for vn in self.asrv.vfs.all_vols.values(): + if vn.histpath == histpath: + cfgi = self.volcfgi(vn) + cfga = self.volcfga(vn) + break + t = "writing thumbnailer-config %d,%d to %s" + self.log(t % (len(cfgi), len(cfga), histpath)) + chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755 + for cfg, cat in ((cfgi, "th"), (cfga, "ac")): + bos.makedirs(os.path.join(histpath, cat), vf=chmod) + with open(os.path.join(histpath, cat, "cfg.txt"), "wb") as f: + f.write(cfg.encode("utf-8")) + def wait4ram(self, need: float, ttpath: str) -> None: ram = self.args.th_ram_max if need > ram * 0.99: @@ -1241,6 +1283,28 @@ class ThumbSrv(object): time.sleep(interval) def clean(self, histpath: str) -> int: + cfgi = cfga = "" + for vn in self.asrv.vfs.all_vols.values(): + if vn.histpath == histpath: + cfgi = self.volcfgi(vn) + cfga = self.volcfga(vn) + break + for cfg, cat in ((cfgi, "th"), (cfga, "ac")): + if not cfg: + continue + try: + with open(os.path.join(histpath, cat, "cfg.txt"), "rb") as f: + oldcfg = f.read().decode("utf-8") + except: + oldcfg = "" + if cfg == oldcfg: + continue + zs = os.path.join(histpath, cat) + if not os.path.exists(zs): + continue + self.log("thumbnailer-config changed; deleting %s" % (zs,), 3) + shutil.rmtree(zs) + ret = 0 for cat in ["th", "ac"]: top = os.path.join(histpath, cat) @@ -1299,7 +1363,7 @@ class ThumbSrv(object): if len(b64) != 24 or len(ts) != 8 or ext not in exts: raise Exception() except: - if f != "dir.txt": + if f != "dir.txt" and f != "cfg.txt": self.log("foreign file in thumbs dir: [{}]".format(fp), 1) continue From 8e2fb05ab86b5f936bf19106cb7345f889e9d84e Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Dec 2025 22:29:33 +0000 Subject: [PATCH 34/67] audioplayer: fix preload in huge folders; it would skip to next folder instead of untruncating --- copyparty/web/browser.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 39126a4a..a25f7a3d 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -2548,6 +2548,9 @@ var mpui = (function () { if (mpl.prescan_evp == evp) throw "evp match"; + if (treectl.trunc) + return treectl.showmore(99999, repreload); + if (mpl.traversals++ > 4) { mpl.prescan_evp = null; toast.inf(10, L.mm_nof); @@ -3024,6 +3027,9 @@ function play(tid, is_ev, seek) { } if (tn >= mp.order.length) { + if (treectl.trunc) + return treectl.showmore(99999, next_song); + if (mpl.pb_mode == 'loop' || ebi('unsearch')) { tn = 0; } @@ -7520,7 +7526,7 @@ var treectl = (function () { catch (ex) { } }; - r.showmore = function (n) { + r.showmore = function (n, cb) { window.removeEventListener('scroll', r.tscroll); console.log('nvis {0} -> {1}'.format(r.nvis, n)); r.nvis = n; @@ -7530,6 +7536,8 @@ var treectl = (function () { setTimeout(function () { r.gentab(get_evpath(), r.lsc); ebi('wrap').style.opacity = CLOSEST ? 'unset' : 1; + if (cb) + cb(); }, 1); }; From 1a9d4c04d5b638c05a957c795a9953bb81f7ce26 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Dec 2025 22:52:18 +0000 Subject: [PATCH 35/67] mediaplayer: cache now-playing tags; fixes copy-to-irc after navigating to another folder --- copyparty/web/browser.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a25f7a3d..17ed0885 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1242,6 +1242,7 @@ var mpl = (function () { "os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl, 'traversals': 0, 'm3ut': '#EXTM3U\n', + 'np': [{'file': 'nothing'}, ['file']], }; bcfg_bind(r, 'one', 'au_one', false, function (v) { if (mp.au) @@ -1438,7 +1439,7 @@ var mpl = (function () { if (!r.os_ctl || !mp.au) return; - var np = get_np()[0], + var np = mpl.np[0], fns = np.file.split(' - '), artist = (np.circle && np.circle != np.artist ? np.circle + ' // ' : '') + (np.artist || (fns.length > 1 ? fns[0] : '')), title = np.title || fns.pop(), @@ -1784,12 +1785,6 @@ function ft2dict(tr, skip) { } -function get_np() { - var tr = QS('#files tr.play'); - return ft2dict(tr, { 'up_ip': 1 }); -}; - - // toggle player widget var widget = (function () { var r = {}, @@ -1847,9 +1842,8 @@ var widget = (function () { ck = irc ? '06' : '', cv = irc ? '07' : '', m = ck + 'np: ', - npr = get_np(), - npk = npr[1], - np = npr[0]; + npk = mpl.np[1], + np = mpl.np[0]; for (var a = 0; a < npk.length; a++) m += (npk[a] == 'file' ? '' : npk[a]).replace(/^\./, '') + '(' + cv + np[npk[a]] + ck + ') // '; @@ -3103,9 +3097,12 @@ function play(tid, is_ev, seek) { for (var a = 0, aa = trs.length; a < aa; a++) clmod(trs[a], 'play'); - var oid = 'a' + tid; - clmod(ebi(oid), 'act', 1); - clmod(ebi(oid).closest('tr'), 'play', 1); + var oid = 'a' + tid, + t_a = ebi(oid), + t_tr = t_a.closest('tr'); + + clmod(t_a, 'act', 1); + clmod(t_tr, 'play', 1); clmod(ebi('wtoggle'), 'np', mpl.clip); clmod(ebi('wtoggle'), 'm3u', mpl.m3uen); if (thegrid) @@ -3127,12 +3124,12 @@ function play(tid, is_ev, seek) { } if (!seek && !ebi('unsearch')) { - var o = ebi(oid); - o.setAttribute('id', 'thx_js'); + t_a.setAttribute('id', 'thx_js'); if (mpl.aplay) sethash(oid + getsort()); - o.setAttribute('id', oid); + t_a.setAttribute('id', oid); } + mpl.np = ft2dict(t_tr, { 'up_ip': 1 }); pbar.unwave(); if (mpl.waves) @@ -3147,7 +3144,7 @@ function play(tid, is_ev, seek) { catch (ex) { toast.err(0, esc(L.mm_playerr + basenames(ex))); } - clmod(ebi(oid), 'act'); + clmod(t_a, 'act'); mpl.t_eplay = setTimeout(next_song, 5000); } From e440578caea9ad179bf350545e9c59a32328af57 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 12 Dec 2025 23:35:21 +0000 Subject: [PATCH 36/67] apply ?nosrvi to #srv_info2 too; closes #1102 --- copyparty/web/browser.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 17ed0885..2d2224b3 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1139,6 +1139,20 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), dk, mp; +var x = ''; +if (!fullui) { + if (window.ui_nombar || /[?&]nombar\b/.exec(sloc0)) x += '#ops,'; + if (window.ui_noacci || /[?&]noacci\b/.exec(sloc0)) x += '#acc_info,'; + if (window.ui_nosrvi || /[?&]nosrvi\b/.exec(sloc0)) x += '#srv_info,#srv_info2,'; + if (window.ui_nocpla || /[?&]nocpla\b/.exec(sloc0)) x += '#goh,'; + if (window.ui_nolbar || /[?&]nolbar\b/.exec(sloc0)) x += '#wfp,'; + if (window.ui_noctxb || /[?&]noctxb\b/.exec(sloc0)) x += '#wtoggle,'; + if (window.ui_norepl || /[?&]norepl\b/.exec(sloc0)) x += '#repl,'; +} +if (x) + document.head.appendChild(mknod('style', '', x.slice(0, -1) + '{display:none!important}')); + + if (location.pathname.indexOf('//') === 0) hist_replace(location.pathname.replace(/^\/+/, '/')); @@ -8231,7 +8245,12 @@ var settheme = (function () { freshen(); }; - freshen(); + var m = /[?&]theme=([0-9]+)/.exec(sloc0); + if (m) + r.go(parseInt(m[1])); + else + freshen(); + return r; })(); @@ -9442,14 +9461,3 @@ function reload_browser() { msel.render(); } treectl.hydrate(); - -if (!fullui && (window.ui_nombar || /[?&]nombar\b/.exec(sloc0))) ebi('ops').style.display = 'none'; -if (!fullui && (window.ui_noacci || /[?&]noacci\b/.exec(sloc0))) ebi('acc_info').style.display = 'none'; -if (!fullui && (window.ui_nosrvi || /[?&]nosrvi\b/.exec(sloc0))) ebi('srv_info').style.display = 'none'; -if (!fullui && (window.ui_nocpla || /[?&]nocpla\b/.exec(sloc0))) ebi('goh').style.display = 'none'; -if (!fullui && (window.ui_nolbar || /[?&]nolbar\b/.exec(sloc0))) ebi('wfp').style.display = 'none'; -if (!fullui && (window.ui_noctxb || /[?&]noctxb\b/.exec(sloc0))) ebi('wtoggle').style.display = 'none'; -if (!fullui && (window.ui_norepl || /[?&]norepl\b/.exec(sloc0))) ebi('repl').style.display = 'none'; - -var m = /[?&]theme=([0-9]+)/.exec(sloc0); -if (m) settheme.go(parseInt(m[1])); From 4b0064b2094244873ca2c49f6a4e94bbac0f865c Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 14:41:16 +0000 Subject: [PATCH 37/67] discard rejected connection --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6f7a57b1..73ceaa82 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -633,7 +633,7 @@ class HttpCli(object): if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"): self.log("illegal relpath; req(%r) => %r" % (self.req, "/" + self.vpath)) self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths") - return self.tx_404() and self.keepalive + return self.tx_404() and False zso = self.headers.get("authorization") bauth = "" From 3bbed1bc46e0345eb4bfef9e720e101546be0a6c Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 16:21:49 +0000 Subject: [PATCH 38/67] fstab: deref fuseblk to real fs --- copyparty/fsutil.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/copyparty/fsutil.py b/copyparty/fsutil.py index d473c016..2151c8c8 100644 --- a/copyparty/fsutil.py +++ b/copyparty/fsutil.py @@ -127,6 +127,24 @@ class Fstab(object): self.log("mtab has changed; reevaluating support for sparse files") + try: + fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"] + if not fuses or MACOS: + raise Exception() + try: + so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6 + except: + so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINTS"]) # future + for ln in so.split("\n"): + zsl = ln.split(" ", 1) + if len(zsl) != 2: + continue + fs, mp = zsl + if mp in fuses: + dtab[mp] = fs + except: + pass + tab1 = list(dtab.items()) tab1.sort(key=lambda x: (len(x[0]), x[0])) path1, fs1 = tab1[0] From ba017f7b532b67fe9d3cd35076e0aba2bb8f3c4a Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 19:44:56 +0000 Subject: [PATCH 39/67] only use fs-legal chars in names (closes #1010); uploading a folder named COMPLE:X into exfat on linux would fail because exfat behaves like windows, rejecting <>:|?*"\/ this would also fail on windows, but then due to sanitize_fn being overly aggressive fix this by detecting filesystem traits on startup and also translating vpath early on windows --- copyparty/__main__.py | 1 + copyparty/cfg.py | 4 +++- copyparty/fsutil.py | 32 ++++++++++++++++++++++++---- copyparty/httpcli.py | 22 +++++++++++++------ copyparty/util.py | 49 ++++++++++++++++++++++++------------------- tests/util.py | 1 + 6 files changed, 77 insertions(+), 32 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 137bfd07..f1881cc1 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1204,6 +1204,7 @@ def add_fs(ap): ap2 = ap.add_argument_group("filesystem options") rm_re_def = "15/0.1" if ANYWIN else "0/0" ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)") + ap2.add_argument("--fsnt", metavar="OS", type=u, default="auto", help="which characters to allow in file/folder names; [\033[32mwin\033[0m] = windows (not <>:|?*\"\\/), [\033[32mmac\033[0m] = macos (not :), [\033[32mlin\033[0m] = linux (anything goes) (volflag=fsnt)") ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)") ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)") ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)") diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 799ab9df..69903225 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -102,6 +102,7 @@ def vf_vmap() -> dict[str, str]: "du_who", "ufavico", "forget_ip", + "fsnt", "hsortn", "html_head", "html_head_s", @@ -202,10 +203,12 @@ flagcats = { "noclone": "take dupe data from clients, even if available on HDD", "nodupe": "rejects existing files (instead of linking/cloning them)", "nodupem": "rejects existing files during moves as well", + "casechk=auto": "actively prevent case-insensitive filesystem? y/n", "chmod_d=755": "unix-permission for new dirs/folders", "chmod_f=644": "unix-permission for new files", "uid=573": "change owner of new files/folders to unix-user 573", "gid=999": "change owner of new files/folders to unix-group 999", + "fsnt=auto": "filesystem filename traits (lin/win/mac/auto)", "wram": "allow uploading into ramdisks", "sparse": "force use of sparse files, mainly for s3-backed storage", "nosparse": "deny use of sparse files, mainly for slow storage", @@ -267,7 +270,6 @@ flagcats = { "no_db_ip": "never store uploader-IP in the db; disables unpost", "fat32": "avoid excessive reindexing on android sdcardfs", "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", - "casechk=auto": "actively prevent case-insensitive filesystem? y/n", "xlink": "cross-volume dupe detection / linking (dangerous)", "xdev": "do not descend into other filesystems", "xvol": "do not follow symlinks leaving the volume root", diff --git a/copyparty/fsutil.py b/copyparty/fsutil.py index 2151c8c8..e5ee78e3 100644 --- a/copyparty/fsutil.py +++ b/copyparty/fsutil.py @@ -2,6 +2,7 @@ from __future__ import print_function, unicode_literals import argparse +import json import os import re import time @@ -9,7 +10,7 @@ import time from .__init__ import ANYWIN, MACOS from .authsrv import AXS, VFS, AuthSrv from .bos import bos -from .util import chkcmd, min_ex, undot +from .util import chkcmd, json_hesc, min_ex, undot if True: # pylint: disable=using-constant-test from typing import Optional, Union @@ -212,19 +213,26 @@ class Fstab(object): return ret.realpath, "" +_fstab: Optional[Fstab] = None +winfs = set(("msdos", "vfat", "ntfs", "exfat")) +# "msdos" = vfat on macos + + def ramdisk_chk(asrv: AuthSrv) -> None: # should have been in authsrv but that's a circular import + global _fstab mods = [] ramfs = ("tmpfs", "overlay") log = asrv.log_func or print - fstab = Fstab(log, asrv.args, False) + if not _fstab: + _fstab = Fstab(log, asrv.args, False) for vn in asrv.vfs.all_nodes.values(): if not vn.axs.uwrite or "wram" in vn.flags: continue ap = vn.realpath if not ap or os.path.isfile(ap): continue - fs, mp = fstab.get(ap) + fs, mp = _fstab.get(ap) mp = "/" + mp.strip("/") if fs == "tmpfs" or (mp == "/" and fs in ramfs): mods.append((vn.vpath, ap, fs, mp)) @@ -234,8 +242,24 @@ def ramdisk_chk(asrv: AuthSrv) -> None: zsl = list(ztsp) zsl[1] = False zsl[2] = False - vn.uaxs[un] = zsl + vn.uaxs[un] = tuple(zsl) if mods: t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:" t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods] log("vfs", t + "".join(t2) + "\n", 1) + + assume = "mac" if MACOS else "lin" + for vol in asrv.vfs.all_nodes.values(): + if not vol.realpath or vol.flags.get("is_file"): + continue + zs = vol.flags["fsnt"].strip()[:3].lower() + if ANYWIN and not zs: + zs = "win" + if zs in ("lin", "win", "mac"): + vol.flags["fsnt"] = zs + continue + fs = _fstab.get(vol.realpath)[0] + fs = "win" if fs in winfs else assume + htm = json.loads(vol.js_htm) + vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs + vol.js_htm = json_hesc(json.dumps(htm)) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 73ceaa82..ff3f4b9a 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -49,6 +49,9 @@ from .util import ( HAVE_SQLITE3, HTTPCODE, UTC, + VPTL_MAC, + VPTL_OS, + VPTL_WIN, Garda, MultipartParser, ODict, @@ -167,6 +170,7 @@ A_FILE = os.stat_result( ) RE_CC = re.compile(r"[\x00-\x1f]") # search always faster +RE_USAFE = re.compile(r'[\x00-\x1f<>"]') # search always faster RE_HSAFE = re.compile(r"[\x00-\x1f<>\"'&]") # search always much faster RE_HOST = re.compile(r"[^][0-9a-zA-Z.:_-]") # search faster <=17ch RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch @@ -515,8 +519,7 @@ class HttpCli(object): self.loud_reply(t, status=400) return False - ptn_cc = RE_CC - m = ptn_cc.search(self.req) + m = RE_USAFE.search(self.req) if m: zs = self.req t = "malicious user; Cc in req0 %r => %r" @@ -538,6 +541,7 @@ class HttpCli(object): vpath = undot(vpath) re_k = RE_K + ptn_cc = RE_CC k_safe = UPARAM_CC_OK for k in arglist.split("&"): if "=" in k: @@ -620,17 +624,18 @@ class HttpCli(object): self.loud_reply("u wot m8", status=400) return False + if VPTL_OS: + vpath = vpath.translate(VPTL_OS) + self.uparam = uparam self.cookies = cookies self.vpath = vpath - self.vpaths = ( - self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath - ) + self.vpaths = vpath + "/" if self.trailing_slash and vpath else vpath if "qr" in uparam: return self.tx_qr() - if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"): + if "\x00" in vpath or (ANYWIN and ("\n" in vpath or "\r" in vpath)): self.log("illegal relpath; req(%r) => %r" % (self.req, "/" + self.vpath)) self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths") return self.tx_404() and False @@ -2807,6 +2812,11 @@ class HttpCli(object): raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again") vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) + fsnt = vfs.flags["fsnt"] + if fsnt != "lin": + tl = VPTL_WIN if fsnt == "win" else VPTL_MAC + rem = rem.translate(tl) + name = name.translate(tl) dbv, vrem = vfs.get_dbv(rem) name = sanitize_fn(name, "") diff --git a/copyparty/util.py b/copyparty/util.py index b16dd548..2b7ee321 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -294,6 +294,23 @@ RE_MEMTOTAL = re.compile("^MemTotal:.* kB") RE_MEMAVAIL = re.compile("^MemAvailable:.* kB") +if PY2: + + def umktrans(s1, s2): + return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)} + +else: + umktrans = str.maketrans + +FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/") +VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\") +APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/") +FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":") +FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None +VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None +APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None + + BOS_SEP = ("%s" % (os.sep,)).encode("ascii") @@ -684,7 +701,7 @@ except Exception as ex: ub64dec = base64.urlsafe_b64decode # type: ignore b64enc = base64.b64encode # type: ignore b64dec = base64.b64decode # type: ignore - if not PY36: + if PY36: print("using fallback base64 codec due to %r" % (ex,)) @@ -2232,32 +2249,22 @@ def sanitize_fn(fn: str, ok: str) -> str: if "/" not in ok: fn = fn.replace("\\", "/").split("/")[-1] - if ANYWIN: - remap = [ - ["<", "<"], - [">", ">"], - [":", ":"], - ['"', """], - ["/", "/"], - ["\\", "\"], - ["|", "|"], - ["?", "?"], - ["*", "*"], - ] - for a, b in [x for x in remap if x[0] not in ok]: - fn = fn.replace(a, b) + if APTL_OS: + fn = fn.translate(APTL_OS) + if ANYWIN: + bad = ["con", "prn", "aux", "nul"] + for n in range(1, 10): + bad += ("com%s lpt%s" % (n, n)).split(" ") - bad = ["con", "prn", "aux", "nul"] - for n in range(1, 10): - bad += ("com%s lpt%s" % (n, n)).split(" ") - - if fn.lower().split(".")[0] in bad: - fn = "_" + fn + if fn.lower().split(".")[0] in bad: + fn = "_" + fn return fn.strip() def sanitize_vpath(vp: str, ok: str) -> str: + if not FNTL_OS: + return vp parts = vp.replace(os.sep, "/").split("/") ret = [sanitize_fn(x, ok) for x in parts] return "/".join(ret) diff --git a/tests/util.py b/tests/util.py index 91be4e0b..d4696893 100644 --- a/tests/util.py +++ b/tests/util.py @@ -193,6 +193,7 @@ class Cfg(Namespace): du_who="all", dk_salt="b" * 16, fk_salt="a" * 16, + fsnt="lin", grp_all="acct", idp_gsep=re.compile("[|:;+,]"), iobuf=256 * 1024, From 594ec39481b20302ba0bb8b7229192effc5f48ef Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 20:09:08 +0000 Subject: [PATCH 40/67] fix ipu with idp users; closes #1094 --- copyparty/authsrv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 1fd5af99..27a7b401 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1812,7 +1812,7 @@ class AuthSrv(object): derive_args(self.args) self.setup_auth_ord() - if self.args.ipu: + if self.args.ipu and not self.args.have_idp_hdrs: # syntax (CIDR=UNAME) is verified in load_ipu zsl = [x.split("=", 1)[1] for x in self.args.ipu] zsl = [x for x in zsl if x not in acct] From 14bef85b87fd1bc85e2339a30adb8e5301c506fa Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 22:05:29 +0000 Subject: [PATCH 41/67] custom logue/md names; closes #1068, closes #1089 --- copyparty/__main__.py | 4 ++++ copyparty/authsrv.py | 12 ++++++++++ copyparty/cfg.py | 8 +++++++ copyparty/httpcli.py | 56 +++++++++++++++++++------------------------ copyparty/up2k.py | 2 +- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f1881cc1..d8ec9246 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1814,6 +1814,10 @@ def add_ui(ap, retry: int): ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)") ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext") ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)") + ap2.add_argument("--prologues", metavar="T,T", type=u, default=".prologue.html", help="comma-sep. list of filenames to scan for and use as prologues (embed above/before directory listing) (volflag=prologues)") + ap2.add_argument("--epilogues", metavar="T,T", type=u, default=".epilogue.html", help="comma-sep. list of filenames to scan for and use as epilogues (embed below/after directory listing) (volflag=epilogues)") + ap2.add_argument("--preadmes", metavar="T,T", type=u, default="preadme.md,PREADME.md", help="comma-sep. list of filenames to scan for and use as preadmes (embed above/before directory listing) (volflag=preadmes)") + ap2.add_argument("--readmes", metavar="T,T", type=u, default="readme.md,README.md", help="comma-sep. list of filenames to scan for and use as readmes (embed below/after directory listing) (volflag=readmes)") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 27a7b401..ff3d2181 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2524,6 +2524,18 @@ class AuthSrv(object): t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s" self.log(t % (vol.vpath, etv), 3) + zsl1 = [x for x in vol.flags["preadmes"].split(",") if x] + zsl2 = [x for x in vol.flags["readmes"].split(",") if x] + zsl3 = list(set([x.lower() for x in zsl1])) + zsl4 = list(set([x.lower() for x in zsl2])) + vol.flags["emb_mds"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] + + zsl1 = [x for x in vol.flags["prologues"].split(",") if x] + zsl2 = [x for x in vol.flags["epilogues"].split(",") if x] + zsl3 = list(set([x.lower() for x in zsl1])) + zsl4 = list(set([x.lower() for x in zsl2])) + vol.flags["emb_lgs"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] + zs = str(vol.flags.get("html_head") or "") if zs and zs[:1] in "%@": vol.flags["html_head_d"] = zs diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 69903225..d83b47f9 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -100,6 +100,7 @@ def vf_vmap() -> dict[str, str]: "chmod_f", "dbd", "du_who", + "epilogues", "ufavico", "forget_ip", "fsnt", @@ -123,8 +124,11 @@ def vf_vmap() -> dict[str, str]: "og_tpl", "og_ua", "opds_exts", + "prologues", + "preadmes", "put_ck", "put_name", + "readmes", "mv_retry", "rm_retry", "shr_who", @@ -333,6 +337,10 @@ flagcats = { "norobots": "kindly asks search engines to leave", "unlistcr": "don't list read-access in controlpanel", "unlistcw": "don't list write-access in controlpanel", + "prologues=.prologue.html": "files to embed above/before files", + "epilogues=.epilogue.html": "files to embed below/after files", + "readmes=readme.md,README.md": "files to embed as readmes", + "preadmes=preadme.md,PREADME.md": "files to embed as preadmes", "no_sb_md": "disable js sandbox for markdown files", "no_sb_lg": "disable js sandbox for prologue/epilogue", "sb_md": "enable js sandbox for markdown files (default)", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index ff3f4b9a..2a36320e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -159,10 +159,6 @@ BADXFF2 = ". Some copyparty features are now disabled as a safety measure." H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" -LOGUES = [[0, ".prologue.html"], [1, ".epilogue.html"]] - -READMES = [[0, ["preadme.md", "PREADME.md"]], [1, ["readme.md", "README.md"]]] - RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"} A_FILE = os.stat_result( @@ -4133,40 +4129,36 @@ class HttpCli(object): self, vn: VFS, abspath: str, lnames: Optional[dict[str, str]] ) -> tuple[list[str], list[str]]: logues = ["", ""] - if not self.args.no_logues: - for n, fn in LOGUES: - if lnames is not None and fn not in lnames: - continue + for n, fns1, fns2 in [] if self.args.no_logues else vn.flags["emb_lgs"]: + for fn in fns1 if lnames is None else fns2: + if lnames is not None: + fn = lnames.get(fn) + if not fn: + continue fn = "%s/%s" % (abspath, fn) - if bos.path.isfile(fn): - logues[n] = read_utf8(self.log, fsenc(fn), False) - if "exp" in vn.flags: - logues[n] = self._expand( - logues[n], vn.flags.get("exp_lg") or [] - ) + if not bos.path.isfile(fn): + continue + logues[n] = read_utf8(self.log, fsenc(fn), False) + if "exp" in vn.flags: + logues[n] = self._expand(logues[n], vn.flags.get("exp_lg") or []) + break readmes = ["", ""] - for n, fns in [] if self.args.no_readme else READMES: + for n, fns1, fns2 in [] if self.args.no_readme else vn.flags["emb_mds"]: if logues[n]: continue - elif lnames is None: - pass - elif fns[0] in lnames: - fns = [lnames[fns[0]]] - else: - fns = [] - - txt = "" - for fn in fns: + for fn in fns1 if lnames is None else fns2: + if lnames is not None: + fn = lnames.get(fn.lower()) + if not fn: + continue fn = "%s/%s" % (abspath, fn) - if bos.path.isfile(fn): - txt = read_utf8(self.log, fsenc(fn), False) - break - - if txt and "exp" in vn.flags: - txt = self._expand(txt, vn.flags.get("exp_md") or []) - - readmes[n] = txt + if not bos.path.isfile(fn): + continue + readmes[n] = read_utf8(self.log, fsenc(fn), False) + if "exp" in vn.flags: + readmes[n] = self._expand(readmes[n], vn.flags.get("exp_md") or []) + break return logues, readmes diff --git a/copyparty/up2k.py b/copyparty/up2k.py index a2c3b233..edcae7eb 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -1148,7 +1148,7 @@ class Up2k(object): ft = "\033[0;32m{}{:.0}" ff = "\033[0;35m{}{:.0}" fv = "\033[0;36m{}:\033[90m{}" - zs = "bcasechk du_iwho ext_th_d html_head html_head_d html_head_s put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" + zs = "bcasechk du_iwho emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" fx = set(zs.split()) fd = vf_bmap() fd.update(vf_cmap()) From 7f82189da946e2a6bb1be39146548cb6eb684166 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 22:17:15 +0000 Subject: [PATCH 42/67] readme: archlinux systemd; closes #1070 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 656c29d2..78a79c64 100644 --- a/README.md +++ b/README.md @@ -2411,6 +2411,8 @@ it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably) +> to start the systemd service, either do `systemctl start --user copyparty` to start it as your own user, or `systemctl start copyparty@bob` to use unix-user `bob` + ## fedora package From 965a4a69495f234bef29972ddf28ef2174cd6454 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 13 Dec 2025 22:35:55 +0000 Subject: [PATCH 43/67] logging: date format; closes #1049 --- copyparty/__main__.py | 1 + copyparty/svchub.py | 3 +++ tests/util.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d8ec9246..ccbc92ec 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1607,6 +1607,7 @@ def add_logging(ap): ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") + ap2.add_argument("--log-date", metavar="TXT", type=u, default="", help="date-format, for example [\033[32m%%Y-%%m-%%d\033[0m] (default is disabled; no date, just HH:MM:SS)") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index a414d750..17f794c0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1558,6 +1558,9 @@ class SvcHub(object): with self.log_mutex: dt = datetime.now(self.tz) if dt.day != self.cday or dt.month != self.cmon: + if self.args.log_date: + zs = dt.strftime(self.args.log_date) + self.log_efmt = "%s %s" % (zs, self.log_efmt.split(" ")[-1]) zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n" zs = zs.format(dt.strftime("%Y-%m-%d")) print(zs, end="") diff --git a/tests/util.py b/tests/util.py index d4696893..bf8f94fc 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,7 +164,7 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) - ex = "ah_alg bname chdir chmod_f chpw_db doctitle df exit favico ipa html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban spinner" From 5e85e3d6289dd70aca2f7b3848efb3847075ede5 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 00:06:54 +0000 Subject: [PATCH 44/67] rss: title/description templating; closes #1047 also closes #1053, a PR which inspired this commit heavily (slightly different approach for flexibility and performance) Co-authored-by: Dawson Jeane --- copyparty/__main__.py | 4 +++- copyparty/cfg.py | 10 +++++++++- copyparty/httpcli.py | 28 +++++++++++++++++++++------- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ccbc92ec..e13f6f92 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1701,7 +1701,9 @@ def add_rss(ap): ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)") ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')") ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all") - ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files") + ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files (volflag=rss_sort)") + ap2.add_argument("--rss-fmt-t", metavar="TXT", type=u, default="{fname}", help="title format (url-param 'rss_fmt_t') (volflag=rss_fmt_t)") + ap2.add_argument("--rss-fmt-d", metavar="TXT", type=u, default="{artist} - {title}", help="description format (url-param 'rss_fmt_d') (volflag=rss_fmt_d)") def add_db_general(ap, hcores): diff --git a/copyparty/cfg.py b/copyparty/cfg.py index d83b47f9..138495dc 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -131,6 +131,9 @@ def vf_vmap() -> dict[str, str]: "readmes", "mv_retry", "rm_retry", + "rss_sort", + "rss_fmt_t", + "rss_fmt_d", "shr_who", "sort", "tail_fd", @@ -393,6 +396,12 @@ flagcats = { "tail_tmax=30": "kill connection after 30 sec", "tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)", }, + "rss": { + "rss": "allow '?rss' URL suffix (experimental)", + "rss_sort=m": "default sort-order (m/u/n/s)", + "rss_fmt_t={fname}": "default title-format", + "rss_fmt_d={album},{.tn}": "default description-format", + }, "others": { "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings", "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', @@ -400,7 +409,6 @@ flagcats = { "dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "dks": "per-directory accesskeys allow browsing into subdirs", "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', - "rss": "allow '?rss' URL suffix (experimental)", "rmagic": "expensive analysis for mimetype accuracy", "shr_who=auth": "who can create shares? no/auth/a", "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 2a36320e..059a37da 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -173,6 +173,7 @@ RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch RE_HR = re.compile(r"[<>\"'&]") RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$") +RE_RSS_KW = re.compile(r"(\{[^} ]+\})") UPARAM_CC_OK = set("doc move tree".split()) @@ -1556,18 +1557,31 @@ class HttpCli(object): ap = "" use_magic = "rmagic" in self.vn.flags + tpl_t = self.uparam.get("fmt_t") or self.vn.flags["rss_fmt_t"] + tpl_d = self.uparam.get("fmt_d") or self.vn.flags["rss_fmt_d"] + kw_t = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_t)] + kw_d = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_d)] + for i in hits: if use_magic: ap = os.path.join(self.vn.realpath, i["rp"]) + tags = i["tags"] iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True) - title = unquotep(i["rp"].split("?")[0].split("/")[-1]) - title = html_escape(title, True, True) - tag_t = str(i["tags"].get("title") or "") - tag_a = str(i["tags"].get("artist") or "") - desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a) - desc = html_escape(desc, True, True) if desc else title - mime = html_escape(guess_mime(title, ap)) + fname = tags["fname"] = unquotep(i["rp"].split("?")[0].split("/")[-1]) + title = tpl_t + desc = tpl_d + for zs1, zs2 in kw_t: + title = title.replace(zs1, str(tags.get(zs2, ""))) + for zs1, zs2 in kw_d: + desc = desc.replace(zs1, str(tags.get(zs2, ""))) + title = html_escape(title.strip(), True, True) + if desc.strip(" -,"): + desc = html_escape(desc.strip(), True, True) + else: + desc = title + + mime = html_escape(guess_mime(fname, ap)) lmod = formatdate(max(0, i["ts"])) zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"]) zs = ( From fecc3fd50783073903175978ae7106a0de1cfcbd Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 00:15:12 +0000 Subject: [PATCH 45/67] rename metadata-property "date" to "tdate"; "date" is reserved for the last-modified-timestamp of each file if extraction of the audio metadata property "date" was enabled (not default), this would have collided; rename the audio prop discovered thanks to #1053 --- copyparty/mtag.py | 2 +- copyparty/util.py | 4 ++-- tests/util.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/copyparty/mtag.py b/copyparty/mtag.py index a22fa0dd..d8d00f1e 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -523,7 +523,7 @@ class MTag(object): ], ".tn": ["tracknumber", "trck", "trkn", "track"], "genre": ["genre", "tcon", "\u00a9gen"], - "date": [ + "tdate": [ "original-release-date", "release-date", "date", diff --git a/copyparty/util.py b/copyparty/util.py index 2b7ee321..3544d219 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -487,9 +487,9 @@ MAGIC_MAP = {"jpeg": "jpg"} DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime" -DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash" +DEF_MTE = ".files,circle,album,.tn,artist,title,tdate,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash" -DEF_MTH = ".vq,.aq,vc,ac,fmt,res,.fps" +DEF_MTH = "tdate,.vq,.aq,vc,ac,fmt,res,.fps" REKOBO_KEY = { diff --git a/tests/util.py b/tests/util.py index bf8f94fc..c2764633 100644 --- a/tests/util.py +++ b/tests/util.py @@ -167,7 +167,7 @@ class Cfg(Namespace): ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) - ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban spinner" + ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban rss_fmt_d rss_fmt_t spinner" ka.update(**{k: "no" for k in ex.split()}) ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm" @@ -209,6 +209,7 @@ class Cfg(Namespace): mv_retry="0/0", rm_retry="0/0", rotf_tz="UTC", + rss_sort="m", s_rd_sz=256 * 1024, s_wr_sz=256 * 1024, shr_who="auth", From 921954037be2354364017269ff889550952a89d8 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 17:17:44 +0000 Subject: [PATCH 46/67] warn that rss requires e2d; closes #1104 --- copyparty/authsrv.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index ff3d2181..0a77b685 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2616,18 +2616,16 @@ class AuthSrv(object): vol.flags[k] = int(vol.flags[k]) if "e2d" not in vol.flags: - if "lifetime" in vol.flags: - t = 'removing lifetime config from volume "/{}" because e2d is disabled' - self.log(t.format(vol.vpath), 1) - del vol.flags["lifetime"] + zs = "lifetime rss" + drop = [x for x in zs.split() if x in vol.flags] - needs_e2d = [x for x in hooks if x in ("xau", "xiu")] - drop = [x for x in needs_e2d if vol.flags.get(x)] - if drop: - t = 'removing [{}] from volume "/{}" because e2d is disabled' - self.log(t.format(", ".join(drop), vol.vpath), 1) - for x in drop: - vol.flags.pop(x) + zs = "xau xiu" + drop += [x for x in zs.split() if vol.flags.get(x)] + + for k in drop: + t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)' + self.log(t % (k, vol.vpath), 1) + vol.flags.pop(k) zi = vol.flags.get("lifetime") or 0 zi2 = time.time() // (86400 * 365) From efc6a09dd3dff2dae93289089ab1f370b3dbbe14 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 17:24:54 +0000 Subject: [PATCH 47/67] allow existing blank chpw.json (closes #1038); previously, would crash on startup if chpw.json exists and is blank, because valid json was enforced now allowing a blank initial file to match the behavior of sqlite --- copyparty/authsrv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 0a77b685..008ff347 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3306,7 +3306,7 @@ class AuthSrv(object): pwdb = {} else: jtxt = read_utf8(self.log, ap, True) - pwdb = json.loads(jtxt) + pwdb = json.loads(jtxt) if jtxt.strip() else {} pwdb = [x for x in pwdb if x[0] != uname] pwdb.append((uname, self.defpw[uname], hpw)) @@ -3330,7 +3330,7 @@ class AuthSrv(object): return jtxt = read_utf8(self.log, ap, True) - pwdb = json.loads(jtxt) + pwdb = json.loads(jtxt) if jtxt.strip() else {} useen = set() urst = set() From 3bc0bf19b009f8dfcc9caaf8459a1f59e1bed008 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 18:28:53 +0000 Subject: [PATCH 48/67] cache-control volflag; closes #964 --- README.md | 2 ++ copyparty/__main__.py | 1 + copyparty/authsrv.py | 1 + copyparty/cfg.py | 2 ++ copyparty/httpcli.py | 6 ++---- tests/util.py | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 78a79c64..b696f14e 100644 --- a/README.md +++ b/README.md @@ -1782,6 +1782,8 @@ notes: * `: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` +* `cachectl` changes how webbrowser will cache responses (the `Cache-Control` response-header); default is `no-cache` which will prevent repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed) + * adding `?cache` to a link will override this with "fully cache this for 69 seconds"; `?cache=321` is 321 seconds, and `?cache=i` is 7 days ## database location diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e13f6f92..b7716852 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1281,6 +1281,7 @@ def add_network(ap): ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") + ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)") ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets") if ANYWIN: ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 008ff347..94248023 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1054,6 +1054,7 @@ class AuthSrv(object): self.is_lxc = args.c == ["/z/initcfg"] self._vf0b = { + "cachectl": self.args.cachectl, "tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho, "shr_who": self.args.shr_who if self.args.shr else "no", diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 138495dc..92b19153 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -95,6 +95,7 @@ def vf_vmap() -> dict[str, str]: } for k in ( "bup_ck", + "cachectl", "casechk", "chmod_d", "chmod_f", @@ -419,6 +420,7 @@ flagcats = { "zipmaxt=no": "reply with 'no' if download-as-zip exceeds max", "zipmaxu": "zip-size-limit does not apply to authenticated users", "nopipe": "disable race-the-beam (download unfinished uploads)", + "cachectl=no-cache": "controls caching in webbrowsers", "mv_retry": "ms-windows: timeout for renaming busy files", "rm_retry": "ms-windows: timeout for deleting busy files", "davauth": "ask webdav clients to login for all folders", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 059a37da..c649c36b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -149,8 +149,6 @@ _ = (argparse, threading) USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {} -NO_CACHE = {"Cache-Control": "no-cache"} - ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" @@ -973,7 +971,7 @@ class HttpCli(object): def permit_caching(self) -> None: cache = self.uparam.get("cache") if cache is None: - self.out_headers.update(NO_CACHE) + self.out_headers["Cache-Control"] = self.vn.flags["cachectl"] return n = 69 if not cache else 604869 if cache == "i" else int(cache) @@ -5159,7 +5157,7 @@ class HttpCli(object): file_ts = int(max(ts_md, self.E.t0)) file_lastmod, do_send, _ = self._chk_lastmod(file_ts) self.out_headers["Last-Modified"] = file_lastmod - self.out_headers.update(NO_CACHE) + self.out_headers["Cache-Control"] = "no-cache" status = 200 if do_send else 304 arg_base = "?" diff --git a/tests/util.py b/tests/util.py index c2764633..c6def6be 100644 --- a/tests/util.py +++ b/tests/util.py @@ -167,7 +167,7 @@ class Cfg(Namespace): ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) - ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban rss_fmt_d rss_fmt_t spinner" + ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl rss_fmt_d rss_fmt_t spinner" ka.update(**{k: "no" for k in ex.split()}) ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm" From 5a1f0a330c21bdd606b795a07e981966de02c8e7 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 18:29:53 +0000 Subject: [PATCH 49/67] readme: faq: volflags, volumes --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b696f14e..55606e03 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,12 @@ upgrade notes * CopyParty? * nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :> +* what is a volflag? + * per-volume configuration; many (not all) global-options can be set as volflags, and most (not all) volflags can be set as global-options; [complete list of volflags](https://copyparty.eu/cli/#flags-help-page) + +* what is a volume? + * a mapping from a URL (`/music/`) to a folder on your server's local filesystem (`/home/ed/Music` or `C:\Users\ed\Music`) which can then be accessed through copyparty, depending on the permissions and options you set on it -- see [accounts and volumes](#accounts-and-volumes) + * can I change the 🌲 spinning pine-tree loading animation? * [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-( @@ -1293,7 +1299,7 @@ using arguments or config files, or a mix of both: * or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume * changes to the `[global]` config section requires a restart to take effect -**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with [`--help`](https://copyparty.eu/cli/) (or click that link) to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in [`--help-flags`](https://copyparty.eu/cli/#flags-help-page) can be used in volumes as volflags. +**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with [`--help`](https://copyparty.eu/cli/) (or click that link) to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in [`--help-flags`](https://copyparty.eu/cli/#flags-help-page) can be used in volumes as volflags (per-volume configuration). * if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help` * or if you prefer plaintext, https://copyparty.eu/helptext.txt From 08474dbe14c8de3e4fc02931c9bb30fe344f9d94 Mon Sep 17 00:00:00 2001 From: stackxp <170874486+stackxp@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:05:22 +0100 Subject: [PATCH 50/67] reject blank password in login ui (#1105) inlines css in msg.html to remove a roundtrip; response now requires multiple tcp-packets but probably always did realistically (https) Co-authored-by: stackxp Co-authored-by: ed --- copyparty/__init__.py | 1 - copyparty/web/msg.css | 36 ------------------------------------ copyparty/web/msg.html | 9 +++++++-- copyparty/web/splash.html | 2 ++ copyparty/web/splash.js | 13 ++++++++++++- scripts/sfx.ls | 1 - 6 files changed, 21 insertions(+), 41 deletions(-) delete mode 100644 copyparty/web/msg.css diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 6baaaf5f..8dba6457 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -86,7 +86,6 @@ web/md2.js web/mde.css web/mde.html web/mde.js -web/msg.css web/msg.html web/opds.xml web/rups.css diff --git a/copyparty/web/msg.css b/copyparty/web/msg.css deleted file mode 100644 index ab8fa4d1..00000000 --- a/copyparty/web/msg.css +++ /dev/null @@ -1,36 +0,0 @@ -:root { - --font-main: sans-serif; - --font-serif: serif; - --font-mono: 'scp'; -} -html,body,tr,th,td,#files,a { - color: inherit; - background: none; - font-weight: inherit; - font-size: inherit; - padding: 0; - border: none; -} -html { - color: #ccc; - background: #333; - font-family: sans-serif; - font-family: var(--font-main), sans-serif; - text-shadow: 1px 1px 0px #000; - touch-action: manipulation; -} -html, body { - margin: 0; - padding: 0; -} -#box { - padding: .5em 1em; - background: #2c2c2c; -} -pre { - font-family: monospace, monospace; - font-family: var(--font-mono), monospace, monospace; -} -a { - color: #fc5; -} diff --git a/copyparty/web/msg.html b/copyparty/web/msg.html index cfae5828..f5d3edc9 100644 --- a/copyparty/web/msg.html +++ b/copyparty/web/msg.html @@ -7,7 +7,12 @@ - + {{ html_head }} @@ -43,7 +48,7 @@ {%- endif %} {%- if js %} diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 9b642b9b..b57ff3f7 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -117,6 +117,7 @@ {%- if ahttps %} switch to https {%- endif %} +
{%- else %} @@ -149,6 +150,7 @@ {%- if ahttps %} switch to https {%- endif %} +
{%- endif %} diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 6185f341..09d4694a 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -8,6 +8,8 @@ Ls.eng = { "ta1": "fill in your new password first", "ta2": "repeat to confirm new password:", "ta3": "found a typo; please try again", + "nop": "ERROR: Password cannot be blank", + "nou": "ERROR: Username and/or password cannot be blank", } }; @@ -95,8 +97,17 @@ if (/\&re=/.test('' + location)) ebi('x').onclick = function (e) { ev(e); if (!pwi.value) - return redo(d.ta1); + return ebi('lm').innerHTML = d.ta1; modal.prompt(d.ta2, "y", mok, null, stars); }; })(); + +if (ebi('lf')) + ebi('lf').onsubmit = function() { + var un = ebi('lu'); + if (ebi('lp').value && (!un || un.value)) + return true; + ebi('lm').innerHTML = un ? d.nou : d.nop; + return false; + }; diff --git a/scripts/sfx.ls b/scripts/sfx.ls index 59a13916..e387844a 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -99,7 +99,6 @@ copyparty/web/md2.js, copyparty/web/mde.css, copyparty/web/mde.html, copyparty/web/mde.js, -copyparty/web/msg.css, copyparty/web/msg.html, copyparty/web/opds.xml, copyparty/web/rups.css, From 56e15009c71fb5289749fb60c3f45f0b8e9f5d58 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 20:14:05 +0000 Subject: [PATCH 51/67] controlpanel: use english for untranslated strings --- README.md | 2 +- copyparty/web/splash.js | 4 ++++ copyparty/web/tl/nor.js | 2 ++ scripts/tl.js | 2 ++ scripts/tl.py | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55606e03..8b9dee9e 100644 --- a/README.md +++ b/README.md @@ -455,7 +455,7 @@ upgrade notes * per-volume configuration; many (not all) global-options can be set as volflags, and most (not all) volflags can be set as global-options; [complete list of volflags](https://copyparty.eu/cli/#flags-help-page) * what is a volume? - * a mapping from a URL (`/music/`) to a folder on your server's local filesystem (`/home/ed/Music` or `C:\Users\ed\Music`) which can then be accessed through copyparty, depending on the permissions and options you set on it -- see [accounts and volumes](#accounts-and-volumes) + * a mapping from a URL (`/music/`) to a folder on your server's local filesystem (`C:\Users\ed\Music`) which can then be accessed through copyparty, depending on the permissions and options you set on it -- see [accounts and volumes](#accounts-and-volumes) * can I change the 🌲 spinning pine-tree loading animation? * [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-( diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 09d4694a..5b3543ae 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -17,6 +17,10 @@ if (window.langmod) langmod(); var d = (Ls[lang] || Ls.eng).splash; +if (Ls.eng && d !== Ls.eng.splash) + for (var k in Ls.eng.splash) + if (d[k] === undefined) + d[k] = Ls.eng.splash[k]; d.wb = d.w; diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index dddd1dae..e7639dc2 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -675,6 +675,8 @@ Ls.nor = { "ta1": "du må skrive et nytt passord først", "ta2": "gjenta for å bekrefte nytt passord:", "ta3": "fant en skrivefeil; vennligst prøv igjen", + "nop": "FEIL: Passord kan ikke være blankt", + "nou": "FEIL: Både brukernavn og passord må angis", "aa1": "innkommende:", "ab1": "skru av no304", "ac1": "skru på no304", diff --git a/scripts/tl.js b/scripts/tl.js index a8c8ea74..28146c09 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -702,6 +702,8 @@ Ls.hmn = { "ta1": "fill in your new password first", "ta2": "repeat to confirm new password:", "ta3": "found a typo; please try again", + "nop": "ERROR: Password cannot be blank", + "nou": "ERROR: Username and/or password cannot be blank", "aa1": "incoming files:", "ab1": "disable no304", "ac1": "enable no304", diff --git a/scripts/tl.py b/scripts/tl.py index ca890d12..52c1c54f 100755 --- a/scripts/tl.py +++ b/scripts/tl.py @@ -105,6 +105,8 @@ Ls.{lang3} = {{ "ta1": "fill in your new password first", "ta2": "repeat to confirm new password:", "ta3": "found a typo; please try again", + "nop": "ERROR: Password cannot be blank", + "nou": "ERROR: Username and/or password cannot be blank", "aa1": "incoming files:", "ab1": "disable no304", "ac1": "enable no304", From 67ddc64171949593c29cae4ab56c2e947c25ad33 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 22:04:46 +0000 Subject: [PATCH 52/67] mtag: replace `keyfinder-py` with `keyfinder-cli`; died in alpine 3.23 due to ffmpeg8 --- bin/mtag/audio-key.py | 22 +++++++++++++++++++--- bin/mtag/install-deps.sh | 7 ++++++- scripts/docker/Dockerfile.dj | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/bin/mtag/audio-key.py b/bin/mtag/audio-key.py index 9d79ebbd..9aad256b 100755 --- a/bin/mtag/audio-key.py +++ b/bin/mtag/audio-key.py @@ -4,13 +4,19 @@ import os import sys import tempfile import subprocess as sp -import keyfinder + +try: + import keyfinder + + PKF = True +except: + PKF = False from copyparty.util import fsenc """ dep: github/mixxxdj/libkeyfinder -dep: pypi/keyfinder +dep: pypi/keyfinder -OR- EvanPurkhiser/keyfinder-cli dep: ffmpeg """ @@ -35,7 +41,17 @@ def det(tf): ]) # fmt: on - print(keyfinder.key(tf).camelot()) + if PKF: + print(keyfinder.key(tf).camelot()) + else: + # fmt: off + sp.check_call([ + b"keyfinder-cli", + b"-n", + b"camelot", + fsenc(tf) + ]) + # fmt: on def main(): diff --git a/bin/mtag/install-deps.sh b/bin/mtag/install-deps.sh index 55473911..811a8263 100755 --- a/bin/mtag/install-deps.sh +++ b/bin/mtag/install-deps.sh @@ -155,6 +155,11 @@ install_keyfinder() { return } + (cat /etc/alpine-release || echo a) 2>&1 | grep -E '3\.2[3-9]' && { + echo "alpine too new; ffmpeg8 is keyfinder-py incompat; giving up" + return + } + cd "$td" github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest ls -al @@ -189,7 +194,7 @@ install_keyfinder() { exit 1 } - x=${-//[^x]/}; set -x; cat /etc/alpine-release + x=${-//[^x]/}; set -x; cat /etc/alpine-release || true # rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder* CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \ CXXFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \ diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index b377537b..68d56dd2 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -18,7 +18,7 @@ RUN apk add -U !pyc \ py3-magic \ vips-jxl vips-heif vips-poppler vips-magick \ py3-numpy fftw libsndfile \ - vamp-sdk vamp-sdk-libs \ + vamp-sdk vamp-sdk-libs keyfinder-cli \ libraw py3-numpy cython \ && apk add -t .bd \ bash wget gcc g++ make cmake patchelf \ From 9e64fe02f90765e70d033bccbad1c68d04f08ec6 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 22:15:53 +0000 Subject: [PATCH 53/67] deps: copyparty.exe: python-3.13.11 --- scripts/pyinstaller/deps.sha512 | 2 +- scripts/pyinstaller/notes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index 13180cce..3b49e889 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -31,5 +31,5 @@ a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac7 fa5d24c51e39760fc5121e56e9948384e03f62b66907ba313a6a803dd601832df62fb5066f3019620664d7cc6b0482e13000cd2d3d1553b709a56a347919565e pillow-12.0.0-cp313-cp313-win_amd64.whl b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl -7d937df1345407398f215c0943514f9dadf2a951b2687e20c06116b2cb3e1d641289da705fcc0b3548f77d19f42f52306c27aa1fc00ed56d19bf47e839c495a5 python-3.13.10-amd64.exe +1735728ae50e003badc5266638e41a73358f2151405e7888b6dc45697c074a60e6e58c8507b49a3f42d8f4fe4005fbc225cd766ab6582cbf85aa79bab699c08f python-3.13.11-amd64.exe 2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 936c9f67..e6ae3cd9 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -42,7 +42,7 @@ fns=( pillow-12.0.0-cp313-cp313-win_amd64.whl pyinstaller-6.15.0-py3-none-win_amd64.whl pyinstaller_hooks_contrib-2025.8-py3-none-any.whl - python-3.13.10-amd64.exe + python-3.13.11-amd64.exe setuptools-80.9.0-py3-none-any.whl ) [ $w7 ] && fns+=( From e0b04d9c16c05dc3ded43597663ef4f7e9512345 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 22:18:43 +0000 Subject: [PATCH 54/67] webdeps: dompurify-3.3.1 --- scripts/deps-docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index 18570e2d..6ea7966e 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /z ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \ ver_hashwasm=4.12.0 \ ver_marked=4.3.0 \ - ver_dompf=3.2.7 \ + ver_dompf=3.3.1 \ ver_mde=2.18.0 \ ver_codemirror=5.65.18 \ ver_fontawesome=5.13.0 \ From 2e7390a4c556b2868779a3590d3d0944ba430a6a Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 22:19:57 +0000 Subject: [PATCH 55/67] new-file: suggest .md rather than .txt --- copyparty/web/tl/chi.js | 2 +- copyparty/web/tl/cze.js | 2 +- copyparty/web/tl/deu.js | 2 +- copyparty/web/tl/epo.js | 2 +- copyparty/web/tl/fin.js | 2 +- copyparty/web/tl/fra.js | 2 +- copyparty/web/tl/grc.js | 2 +- copyparty/web/tl/ita.js | 2 +- copyparty/web/tl/kor.js | 2 +- copyparty/web/tl/nld.js | 2 +- copyparty/web/tl/nno.js | 2 +- copyparty/web/tl/nor.js | 2 +- copyparty/web/tl/pol.js | 2 +- copyparty/web/tl/por.js | 2 +- copyparty/web/tl/rus.js | 2 +- copyparty/web/tl/spa.js | 2 +- copyparty/web/tl/swe.js | 2 +- copyparty/web/tl/tur.js | 2 +- copyparty/web/tl/ukr.js | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/copyparty/web/tl/chi.js b/copyparty/web/tl/chi.js index c5822f89..6987904d 100644 --- a/copyparty/web/tl/chi.js +++ b/copyparty/web/tl/chi.js @@ -440,7 +440,7 @@ Ls.chi = { "fcp_both_b": '复制上传', //m "mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p", - "nmd_i1": "还可以添加需要的文件扩展名,例如 .txt", //m + "nmd_i1": "还可以添加需要的文件扩展名,例如 .md", //m "nmd_i2": "由于没有删除权限,你只能创建 .md 文件", //m "tv_load": "加载文本文件:\n\n{0}\n\n{1}% ({2} 的 {3} MiB 已加载)", diff --git a/copyparty/web/tl/cze.js b/copyparty/web/tl/cze.js index d789b09f..f0fad123 100644 --- a/copyparty/web/tl/cze.js +++ b/copyparty/web/tl/cze.js @@ -444,7 +444,7 @@ Ls.cze = { "fcp_both_b": 'KopírovatNahrát', "mk_noname": "napište název do textového pole vlevo předtím než to uděláte :p", - "nmd_i1": "můžeš také přidat příponu souboru, například .txt", //m + "nmd_i1": "můžeš také přidat příponu souboru, například .md", //m "nmd_i2": "můžeš vytvářet pouze .md soubory, protože nemáš oprávnění mazat", //m "tv_load": "Načítání textového dokumentu:\n\n{0}\n\n{1}% ({2} z {3} MiB načteno)", diff --git a/copyparty/web/tl/deu.js b/copyparty/web/tl/deu.js index df7b87ac..15efa50d 100644 --- a/copyparty/web/tl/deu.js +++ b/copyparty/web/tl/deu.js @@ -440,7 +440,7 @@ Ls.deu = { "fcp_both_b": 'KopierenHochladen', "mk_noname": "Tipp' mal vorher lieber einen Namen in das Textfeld links, bevor du das machst :p", - "nmd_i1": "Fügen Sie auch die gewünschte Dateiendung hinzu, z. B. .txt", //m + "nmd_i1": "Fügen Sie auch die gewünschte Dateiendung hinzu, z. B. .md", //m "nmd_i2": "Sie können nur .md-Dateien erstellen, da Ihnen die Löschberechtigung fehlt", //m "tv_load": "Textdatei wird geladen:\n\n{0}\n\n{1}% ({2} von {3} MiB geladen)", diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index 5f53c39c..322cb9a9 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -440,7 +440,7 @@ Ls.epo = { "fcp_both_b": 'KopiiAlŝuti', "mk_noname": "tajpu nomon en tekstokampo maldekstre antaŭ vi faras ĉi tion :p", - "nmd_i1": "vi povas aldoni la deziratan sufikson, ekzemple .txt", //m + "nmd_i1": "vi povas aldoni la deziratan sufikson, ekzemple .md", //m "nmd_i2": "vi povas krei nur .md-dosierojn ĉar vi ne havas forigan permeson", //m "tv_load": "Ŝargado de teksto-dokumento:\n\n{0}\n\n{1}% ({2} da {3} MiB ŝargita)", diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index d8e4fe31..1088cc62 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -440,7 +440,7 @@ Ls.fin = { "fcp_both_b": 'KopioiLähetä', "mk_noname": "kirjoita nimi vasemmalla olevaan tekstikenttään ennen kuin teet tuon :p", - "nmd_i1": "voit myös lisätä haluamasi tiedostopäätteen, esimerkiksi .txt", //m + "nmd_i1": "voit myös lisätä haluamasi tiedostopäätteen, esimerkiksi .md", //m "nmd_i2": "voit luoda vain .md-tiedostoja, koska sinulla ei ole poistolupaa", //m "tv_load": "Ladataan tekstidokumenttia:\n\n{0}\n\n{1}% ({2} / {3} Mt ladattu)", diff --git a/copyparty/web/tl/fra.js b/copyparty/web/tl/fra.js index 7731671e..a090a3f0 100644 --- a/copyparty/web/tl/fra.js +++ b/copyparty/web/tl/fra.js @@ -440,7 +440,7 @@ Ls.fra = { "fcp_both_b": 'CopierTéléverser', "mk_noname": "entrez un nom dans le champ de texte à gauche avant de faire ça :p", - "nmd_i1": "ajoutez aussi l’extension souhaitée, par exemple .txt", //m + "nmd_i1": "ajoutez aussi l’extension souhaitée, par exemple .md", //m "nmd_i2": "vous ne pouvez créer que des fichiers .md car vous n’avez pas la permission d’effacer", //m "tv_load": "Chargement du document texte:\n\n{0}\n\n{1}% ({2} de {3} MiB chargés)", diff --git a/copyparty/web/tl/grc.js b/copyparty/web/tl/grc.js index c74ef430..27ef5edf 100644 --- a/copyparty/web/tl/grc.js +++ b/copyparty/web/tl/grc.js @@ -440,7 +440,7 @@ Ls.grc = { "fcp_both_b": 'ΑντιγραφήΜεταφόρτωση', "mk_noname": "γράψε ένα όνομα στο πεδίο κειμένου αριστερά πριν το κάνεις :p", - "nmd_i1": "μπορείτε επίσης να προσθέσετε την κατάληξη που θέλετε, όπως .txt", //m + "nmd_i1": "μπορείτε επίσης να προσθέσετε την κατάληξη που θέλετε, όπως .md", //m "nmd_i2": "μπορείτε να δημιουργήσετε μόνο αρχεία .md επειδή δεν έχετε δικαίωμα διαγραφής", //m "tv_load": "Φόρτωση αρχείου κειμένου:\n\n{0}\n\n{1}% ({2} από {3} MiB φορτωμένα)", diff --git a/copyparty/web/tl/ita.js b/copyparty/web/tl/ita.js index 3b12eb9e..a65020de 100644 --- a/copyparty/web/tl/ita.js +++ b/copyparty/web/tl/ita.js @@ -440,7 +440,7 @@ Ls.ita = { "fcp_both_b": 'CopiaCarica', "mk_noname": "scrivi un nome nel campo di testo a sinistra prima di farlo :p", - "nmd_i1": "puoi anche aggiungere l’estensione che vuoi, per esempio .txt", //m + "nmd_i1": "puoi anche aggiungere l’estensione che vuoi, per esempio .md", //m "nmd_i2": "puoi creare solo file .md perché non hai il permesso di eliminare", //m "tv_load": "Caricando documento di testo:\n\n{0}\n\n{1}% ({2} di {3} MiB caricati)", diff --git a/copyparty/web/tl/kor.js b/copyparty/web/tl/kor.js index 00249b30..7321d25c 100644 --- a/copyparty/web/tl/kor.js +++ b/copyparty/web/tl/kor.js @@ -440,7 +440,7 @@ Ls.kor = { "fcp_both_b": '복사업로드', "mk_noname": "왼쪽 텍스트 필드에 이름을 먼저 입력해주세요 :p", - "nmd_i1": "원하는 파일 확장자를 추가할 수 있습니다. 예: .txt", //m + "nmd_i1": "원하는 파일 확장자를 추가할 수 있습니다. 예: .md", //m "nmd_i2": "삭제 권한이 없어서 .md 파일만 만들 수 있습니다", //m "tv_load": "텍스트 문서 불러오는 중:\n\n{0}\n\n{1}% ({3} MiB 중 {2} MiB 로드됨)", diff --git a/copyparty/web/tl/nld.js b/copyparty/web/tl/nld.js index b21897a4..666bed4a 100644 --- a/copyparty/web/tl/nld.js +++ b/copyparty/web/tl/nld.js @@ -440,7 +440,7 @@ Ls.nld = { "fcp_both_b": 'KopieerUpload', "mk_noname": "Voer een naam in het tekstveld aan de linkerkant voordat je verder gaat :p", - "nmd_i1": "Voeg ook de gewenste extensie toe, bijvoorbeeld .txt", //m + "nmd_i1": "Voeg ook de gewenste extensie toe, bijvoorbeeld .md", //m "nmd_i2": "Je kunt alleen .md-bestanden maken omdat je geen verwijderrechten hebt", //m "tv_load": "Tekstdocument laden:\n\n{0}\n\n{1}% ({2} van de {3} MiB geladen)", diff --git a/copyparty/web/tl/nno.js b/copyparty/web/tl/nno.js index eceddbc2..18ff9ac5 100644 --- a/copyparty/web/tl/nno.js +++ b/copyparty/web/tl/nno.js @@ -437,7 +437,7 @@ Ls.nno = { "fcp_both_b": 'KopiérLast opp', "mk_noname": "skriv inn eit namn i tekstboksa åt venstre først :p", - "nmd_i1": "leggja også til filendinga du vil, til dømes .txt", //m + "nmd_i1": "leggja også til filendinga du vil, til dømes .md", //m "nmd_i2": "du kan berre laga .md-filer fordi du ikkje har delete-tilgang", //m "tv_load": "Lastar inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lasta ned)", diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index e7639dc2..f943b3b8 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -437,7 +437,7 @@ Ls.nor = { "fcp_both_b": 'KopiérLast opp', "mk_noname": "skriv inn et navn i tekstboksen til venstre først :p", - "nmd_i1": "legg også til ønsket filtype, for eksempel .txt", //m + "nmd_i1": "legg også til ønsket filtype, for eksempel .md", //m "nmd_i2": "du kan bare lage .md-filer fordi du ikke har delete-tilgang", //m "tv_load": "Laster inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lastet ned)", diff --git a/copyparty/web/tl/pol.js b/copyparty/web/tl/pol.js index 73273388..1abcea68 100644 --- a/copyparty/web/tl/pol.js +++ b/copyparty/web/tl/pol.js @@ -443,7 +443,7 @@ Ls.pol = { "fcp_both_b": 'KopiujPrześlij', "mk_noname": "wpisz nazwę do pola po lewej zanim to zrobisz :p", - "nmd_i1": "możesz też dodać wybrane rozszerzenie, np. .txt", //m + "nmd_i1": "możesz też dodać wybrane rozszerzenie, np. .md", //m "nmd_i2": "możesz tworzyć tylko pliki .md, ponieważ nie masz uprawnień do usuwania", //m "tv_load": "Wczytywanie pliku tekstowego:\n\n{0}\n\n{1}% (wczytano {2} z {3} MiB)", diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index 8c7def8d..bb531490 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -440,7 +440,7 @@ Ls.por = { "fcp_both_b": 'CopiarEnviar', "mk_noname": "digite um nome no campo de texto à esquerda antes de fazer isso :p", - "nmd_i1": "também pode adicionar a extensão desejada, por exemplo .txt", //m + "nmd_i1": "também pode adicionar a extensão desejada, por exemplo .md", //m "nmd_i2": "só pode criar ficheiros .md porque não tem permissão para apagar", //m "tv_load": "Carregando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB carregados)", diff --git a/copyparty/web/tl/rus.js b/copyparty/web/tl/rus.js index fa9c9abd..0c1a6b15 100644 --- a/copyparty/web/tl/rus.js +++ b/copyparty/web/tl/rus.js @@ -440,7 +440,7 @@ Ls.rus = { "fcp_both_b": 'СкопироватьЗагрузить', "mk_noname": "введите имя в текстовое поле слева перед тем, как это делать :p", - "nmd_i1": "вы также можете указать нужное расширение, например .txt", //m + "nmd_i1": "вы также можете указать нужное расширение, например .md", //m "nmd_i2": "вы можете создавать только файлы .md, так как у вас нет разрешения на удаление", //m "tv_load": "Загружаю текстовый документ:\n\n{0}\n\n{1}% ({2} из {3} МиБ загружено)", diff --git a/copyparty/web/tl/spa.js b/copyparty/web/tl/spa.js index 6c4741bd..2fed11b5 100644 --- a/copyparty/web/tl/spa.js +++ b/copyparty/web/tl/spa.js @@ -439,7 +439,7 @@ Ls.spa = { "fcp_both_b": "CopiarSubir", "mk_noname": "escribe un nombre en el campo de texto de la izquierda antes de hacer eso :p", - "nmd_i1": "también puedes añadir la extensión que quieras, por ejemplo .txt", //m + "nmd_i1": "también puedes añadir la extensión que quieras, por ejemplo .md", //m "nmd_i2": "solo puedes crear archivos .md porque no tienes permiso para borrar", //m "tv_load": "Cargando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB cargados)", diff --git a/copyparty/web/tl/swe.js b/copyparty/web/tl/swe.js index c48e3f5f..5f3ad313 100644 --- a/copyparty/web/tl/swe.js +++ b/copyparty/web/tl/swe.js @@ -440,7 +440,7 @@ Ls.swe = { "fcp_both_b": 'KopieraLadda upp', "mk_noname": "skriv ett namn i fältet till vänster först :p", - "nmd_i1": "lägg också till filändelsen du vill ha, till exempel .txt", //m + "nmd_i1": "lägg också till filändelsen du vill ha, till exempel .md", //m "nmd_i2": "du kan bara skapa .md-filer eftersom du inte har borttagningsbehörighet", //m "tv_load": "Laddar textfil:\n\n{0}\n\n{1}% ({2} av {3} MiB laddat)", diff --git a/copyparty/web/tl/tur.js b/copyparty/web/tl/tur.js index 677c013b..ef45f717 100644 --- a/copyparty/web/tl/tur.js +++ b/copyparty/web/tl/tur.js @@ -440,7 +440,7 @@ Ls.tur = { "fcp_both_b": 'KopyalaYükle', "mk_noname": "bunu yapmadan önce soldaki boşluğa bir şeyler yazsana :p", - "nmd_i1": "ayrıca istediğin dosya uzantısını ekleyebilirsin, örneğin .txt", //m + "nmd_i1": "ayrıca istediğin dosya uzantısını ekleyebilirsin, örneğin .md", //m "nmd_i2": "silme iznin olmadığı için yalnızca .md dosyaları oluşturabilirsin", //m "tv_load": "Metin belgesi yükleniyor:\n\n{0}\n\n{1}% ({2} of {3} MiB yüklendi)", diff --git a/copyparty/web/tl/ukr.js b/copyparty/web/tl/ukr.js index 46805bed..1416769a 100644 --- a/copyparty/web/tl/ukr.js +++ b/copyparty/web/tl/ukr.js @@ -440,7 +440,7 @@ Ls.ukr = { "fcp_both_b": 'СкопіюватиЗавантажити', "mk_noname": "введіть ім'я в текстове поле зліва перед тим, як робити це :p", - "nmd_i1": "ви також можете додати потрібне розширення, наприклад .txt", //m + "nmd_i1": "ви також можете додати потрібне розширення, наприклад .md", //m "nmd_i2": "ви можете створювати тільки файли .md, оскільки не маєте дозволу на видалення", //m "tv_load": "Завантаження текстового документа:\n\n{0}\n\n{1}% ({2} з {3} MiB завантажено)", From a3eec23cef45a5fc8a3760e66428112d2f1f80f2 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 23:04:36 +0000 Subject: [PATCH 56/67] v1.19.22 --- copyparty/__version__.py | 4 +-- docs/changelog.md | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 0272b761..76c57f6f 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 21) +VERSION = (1, 19, 22) CODENAME = "usernames" -BUILD_DT = (2025, 12, 2) +BUILD_DT = (2025, 12, 14) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index 65366fa1..66269f92 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,70 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-1202-2047 `v1.19.21` tadaimback + +## 🧪 new features + +* [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#readme) now behave more usefully/predictably; 889bd324 + * hooks returning `0` will run the next hook (if any), and let the initiating action proceed if no other hooks object + * hooks returning `100` will stop processing successive hooks, but return success, letting the initiating action proceed + * hooks returning anything else will stop processing successive hooks (like the documentation always said) and also fail the initiating action (if hook is checked) + * zmq hooks can now respond with json, doing relocations and all that stuff +* new mtag plugin, [geotag.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/geotag.py): read image geotags with exiftool ([demo](https://a.ocv.me/pub/blog/j8/11/)) 1c15c0d5 ac085b81 +* #972 markdown-links are rewritten to open in the markdown-viewer 278a0d85 +* #794 add json beautifier / minifier + * ...in the textfile-editor fd8c5bfc + * ...in the textfile-viewer 89cab5b5 +* #1058 ui-option and server-config to force download instead of showing files inline a9174e5d +* option `stats-u` to grant access to prometheus-metrics based on username, not just permissions b427d780 + +## 🩹 bugfixes + +* #1003 u2c.py (commandline uploader) did not install correctly on archlinux and/or pypi 9385daea +* #1035 uploader could fail to initialize if: 98701b78 + * the `mt` button (webworkers) was enabled in the settings tab + * **and** the network was severely strained during intial page load +* possible deadlock on shutdown if thumbnailer queue was hella busy fb9f0441 +* #971 windows: fix deadlock on startup if trying to use a nonexistant driveletter as a volume 945b2276 +* #1022 js-panic if audio playback is set to stay-in-folder a28503e8 +* links to ongoing file transfers in the controlpanel could 404 (thx @Habetdin!) 77f74ddb f4d67ff0 +* video scrubbing on iOS dba7c5d4 +* #1054 audio volume slider could skip one percent (thx @shermanhlc!) ca6d3a5c +* detect invalid config: + * #959 panic if `ipu` user doesn't exist 79e10786 + * panic if share config overlaps with a volume cedfc444 +* #943 + +## 🔧 other changes + +* the "new-markdown" feature was repurposed into "new-file", accepting any file extension 7d62335c +* #1023 the option to grant delete-access when creating a share was removed due to never having been implemented in the backend 04ac7fbd +* #1012 rephrased the controlpanel login-text when logged in to avoid confusion 7a291403 +* add hints that the serverlog is a good place to look in some situations c424a55d +* all thumbnail types and combinations can now be pregenerated a359b89e +* #1030 add debug if cfssl is misbehaving ec00dc18 +* #871 `grid` volflag is applied during navigation if user has not set a preference a9378a8e +* cosmetic: + * show column number in markdown editor b9aacba1 + * reduced grid margins in theme2 e469bc94 + * reduced redirect delay after logging in f7e7b03f + * controlpanel greeting in some fail-early responses acde21d4 + * update hooks to ignore the new upload-queue-empty message 3f4b79ff +* docs: + * #1032 fix typo in example docker idp config (thx @tuetenk0pp!) 867237d0 + * warn that using/changing `-j` is usually a bad idea cad15fbf + * add hotlink anchors to https://copyparty.eu/cli/ 7f9c139e +* nixos: + * #868 option to install from git-head (thx @shelvacu!) c7345308 + * #962 support idp volumes (thx @nicomem!) 904c984b + * #963 use configured chmod-d when creating volumes (thx @nicomem!) 3242145e +* copyparty.exe: update to python 3.13.10, pillow 12.0 cdffde78 + +## 🌠 fun facts + +* copyparty has been observed running [on a wristwatch](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/clockyparty.jpg) and on an [android tv-box](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg) running in big-endian mode, so copyparty is [BE-certified](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png) +* also... **it's december!** [you know what that means](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) :^) + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-1102-0109 `v1.19.20` november From 3476d5e6d98bab8dd8dcb41f0f3a6c8bc8cd3cf5 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 23:07:26 +0000 Subject: [PATCH 57/67] update pkgs to 1.19.22 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/makedeb-mpr/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 6929aa77..fecef584 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -3,7 +3,7 @@ # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty -pkgver="1.19.21" +pkgver="1.19.22" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) -sha256sums=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42") +sha256sums=("ba2e5c332b5481aa93f1bb9d5e79dfe1a6ed4329e470efac73e685fd3cc3a370") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 37cf79a8..f40f33ff 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.21 +pkgver=1.19.22 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) -sha256sums=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42") +sha256sums=("ba2e5c332b5481aa93f1bb9d5e79dfe1a6ed4329e470efac73e685fd3cc3a370") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 361019f1..017c946d 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.19.21/copyparty-1.19.21.tar.gz", - "version": "1.19.21", - "hash": "sha256-RHI6gj8hjlKq7GB1aVlzp1uGY8kgLID9c/SOUsYazUI=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.22/copyparty-1.19.22.tar.gz", + "version": "1.19.22", + "hash": "sha256-ui5cMytUgaqT8budXnnf4abtQynkcO+sc+aF/TzDo3A=" } \ No newline at end of file From b60eb3f01a258c2eca4e387582c3e4fef2631f1e Mon Sep 17 00:00:00 2001 From: thatfrozenfrog <101154752+thatfrozenfrog@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:52:07 +0700 Subject: [PATCH 58/67] add Vietnamese translation (#1080) --- copyparty/web/tl/vie.js | 727 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 copyparty/web/tl/vie.js diff --git a/copyparty/web/tl/vie.js b/copyparty/web/tl/vie.js new file mode 100644 index 00000000..a11f34e6 --- /dev/null +++ b/copyparty/web/tl/vie.js @@ -0,0 +1,727 @@ +Ls.vie = { + "tt": "Tiếng Việt", + + "cols": { + "c": "nút hành động", + "dur": "thời lượng", + "q": "chất lượng / bitrate", + "Ac": "codec âm thanh", + "Vc": "codec video", + "Fmt": "định dạng / container", + "Ahash": "checksum âm thanh", + "Vhash": "checksum video", + "Res": "độ phân giải", + "T": "loại tệp", + "aq": "chất lượng âm thanh / bitrate", + "vq": "chất lượng video / bitrate", + "pixfmt": "subsampling / pixel structure", + "resw": "độ phân giải ngang", + "resh": "độ phân giải dọc", + "chs": "kênh âm thanh", + "hz": "tốc độ lấy mẫu", + }, + + "hks": [ + [ + "misc", + ["ESC", "đóng nhiều mục"], + + "file-manager", + ["G", "chuyển đổi chế độ xem danh sách / lưới"], + ["T", "chuyển đổi ảnh thu nhỏ / biểu tượng"], + ["⇧ A/D", "kích thước ảnh thu nhỏ"], + ["ctrl-K", "xoá mục đã chọn"], + ["ctrl-X", "cắt mục đã chọn vào bảng nhớ tạm"], + ["ctrl-C", "sao chép mục đã chọn vào bảng nhớ tạm"], + ["ctrl-V", "dán (di chuyển/sao chép) tại đây"], + ["Y", "tải xuống mục đã chọn"], + ["F2", "đổi tên mục đã chọn"], + + "file-list-sel", + ["space", "chuyển đổi chọn tệp"], + ["↑/↓", "di chuyển con trỏ chọn"], + ["ctrl ↑/↓", "di chuyển con trỏ và khung nhìn"], + ["⇧ ↑/↓", "chọn tệp trước / sau"], + ["ctrl-A", "chọn tất cả tệp / thư mục"], + ], [ + "navigation", + ["B", "chuyển đổi đường dẫn / thanh điều hướng"], + ["I/K", "thư mục trước / sau"], + ["M", "thư mục cha (hoặc thu gọn hiện tại)"], + ["V", "chuyển đổi thư mục / tệp văn bản trong thanh điều hướng"], + ["A/D", "kích thước thanh điều hướng"], + ], [ + "audio-player", + ["J/L", "bài trước / sau"], + ["U/O", "lùi / tiến 10 giây"], + ["0..9", "nhảy đến 0%..90%"], + ["P", "phát/tạm dừng (cũng khởi động)"], + ["S", "chọn bài đang phát"], + ["Y", "tải xuống bài hát"], + ], [ + "image-viewer", + ["J/L, ←/→", "ảnh trước / sau"], + ["Home/End", "ảnh đầu / cuối"], + ["F", "toàn màn hình"], + ["R", "xoay theo chiều kim đồng hồ"], + ["⇧ R", "xoay ngược chiều kim đồng hồ"], + ["S", "chọn ảnh"], + ["Y", "tải xuống ảnh"], + ], [ + "video-player", + ["U/O", "lùi / tiến 10 giây"], + ["P/K/Space", "phát/tạm dừng"], + ["C", "tiếp tục phát bài tiếp theo"], + ["V", "vòng lặp"], + ["M", "tắt tiếng"], + ["[ and ]", "đặt khoảng lặp"], + ], [ + "textfile-viewer", + ["I/K", "tệp trước / sau"], + ["M", "đóng tệp văn bản"], + ["E", "chỉnh sửa tệp văn bản"], + ["S", "chọn tệp (để cắt/sao chép/đổi tên)"], + ] + ], + + "m_ok": "OK", + "m_ng": "Hủy", + + "enable": "Bật", + "danger": "NGUY HIỂM", + "clipped": "đã sao chép vào bảng nhớ tạm", + + "ht_s1": "giây", + "ht_s2": "giây", + "ht_m1": "phút", + "ht_m2": "phút", + "ht_h1": "giờ", + "ht_h2": "giờ", + "ht_d1": "ngày", + "ht_d2": "ngày", + "ht_and": " và ", + + "goh": "bảng điều khiển", + "gop": 'thư mục trước">trước', + "gou": 'thư mục cha">lên', + "gon": 'thư mục sau">tiếp', + "logout": "Đăng xuất ", + "login": "Đăng nhập", + "access": "quyền truy cập", + "ot_close": "đóng menu con", + + "ot_search": "tìm kiếm các tệp theo thuộc tính, đường dẫn / tên, tag nhạc hoặc bất kỳ sự kết hợp nào của chúng$N$N<code>foo bar</code> = phải chứa cả «foo» và «bar»,$N<code>foo -bar</code> = phải chứa «foo» nhưng không chứa «bar»,$N<code>^yana .opus$</code> = bắt đầu bằng «yana» và là tệp «opus»$N<code>"try unite"</code> = chứa chính xác «try unite»$N$Nđịnh dạng ngày là iso-8601, như$N<code>2009-12-31</code> hoặc <code>2020-09-12 23:30:00</code>", + + + "ot_unpost": "unpost: xoá các tệp đã tải lên gần đây hoặc huỷ những tệp đang tải dở", + "ot_bup": "bup: trình tải lên cơ bản, hỗ trợ cả Netscape 4.0", + "ot_mkdir": "mkdir: tạo thư mục mới", + "ot_md": "new-file: tạo tệp văn bản mới", + "ot_msg": "msg: gửi tin nhắn đến nhật ký máy chủ", + "ot_mp": "tuỳ chọn trình phát phương tiện", + "ot_cfg": "tuỳ chọn cấu hình", + "ot_u2i": 'up2k: tải tệp lên (nếu bạn có quyền ghi) hoặc chuyển sang chế độ tìm kiếm để xem chúng có tồn tại ở đâu đó trên máy chủ không$N$Ntải lên có thế tiếp tục nếu bị gián đoạn, chạy đa luồng và giữ nguyên dấu thời gian tệp, nhưng tiêu tốn nhiều CPU hơn [🎈]  (trình tải lên cơ bản)

trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!', + "ot_u2w": 'up2k: tải tệp lên với hỗ trợ tiếp tục (đóng trình duyệt và thả lại tệp đó lên sau)$N$Nchạy đa luồng và giữ nguyên dấu thời gian tệp, nhưng tiêu tốn nhiều CPU hơn [🎈]  (trình tải lên cơ bản)

trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!', + "ot_noie": 'Vui lòng sử dụng Chrome / Firefox / Edge', + + "ab_mkdir": "tạo thư mục", + "ab_mkdoc": "tạo tệp văn bản", + "ab_msg": "gửi tin nhắn đến nhật ký máy chủ", + + "ay_path": "bỏ qua đến thư mục", + "ay_files": "bỏ qua đến tệp", + + "wt_ren": "đổi tên các mục đã chọn$NPhím tắt: F2", + "wt_del": "xóa các mục đã chọn$NPhím tắt: ctrl-K", + "wt_cut": "cắt các mục đã chọn <small>(sau đó dán ở nơi khác)</small>$NPhím tắt: ctrl-X", + "wt_cpy": "sao chép các mục đã chọn vào bảng nhớ tạm$N(để dán ở nơi khác)$NPhím tắt: ctrl-C", + "wt_pst": "dán một lựa chọn đã cắt / sao chép trước đó$NPhím tắt: ctrl-V", + "wt_selall": "chọn tất cả các tệp$NPhím tắt: ctrl-A (khi tệp được chọn)", + "wt_selinv": "đảo ngược lựa chọn", + "wt_zip1": "tải thư mục này dưới định dạng nén", + "wt_selzip": "tải lựa chọn dưới định dạng nén", + "wt_seldl": "tải lựa chọn dưới dạng các tệp riêng biệt$NPhím tắt: Y", + "wt_npirc": "sao chép thông tin bản nhạc theo định dạng irc", + "wt_nptxt": "sao chép thông tin bản nhạc dưới dạng văn bản thuần túy", + "wt_m3ua": "thêm vào danh sách phát m3u (bấm 📻copy sau)", + "wt_m3uc": "sao chép danh sách phát m3u vào bảng nhớ tạm", + "wt_grid": "chuyển đổi chế độ xem danh sách / lưới $NPhím tắt: G", + "wt_prev": "bài trước$NPhím tắt: J", + "wt_play": "phát / tạm dừng$NPhím tắt: P", + "wt_next": "bài sau$NPhím tắt: L", + + "ul_par": "tải lên song song:", + "ut_rand": "ngẫu nhiên hoá tên tệp", + "ut_u2ts": "sao chép dấu thời gian chỉnh sửa cuối$Ntừ hệ thống tệp của bạn lên máy chủ\">📅", + "ut_ow": "ghi đè các tệp đã có trên máy chủ?$N🛡️: không bao giờ (sẽ tạo tên tệp mới)$N🕒: ghi đè nếu tệp trên máy chủ cũ hơn$N♻️: luôn ghi đè nếu hai tệp khác nhau", + "ut_mt": "tiếp tục hash các tệp khác trong khi tải lên$N$NCó thể tắt nếu CPU hoặc HDD của bạn bị nghẽn", + "ut_ask": 'yêu cầu xác nhận trước khi bắt đầu tải lên">💭', + "ut_pot": "cải thiện tốc độ tải lên trên các thiết bị chậm$Nbằng cách đơn giản hoá giao diện người dùng", + "ut_srch": "không tải lên, chỉ kiểm tra xem tệp$Nđã tồn tại trên máy chủ hay chưa (sẽ quét toàn bộ thư mục bạn có quyền đọc)", + "ut_par": "tạm dừng tải lên bằng cách đặt thành 0$N$NTăng lên nếu kết nối chậm hoặc độ trễ cao$N$NGiữ ở mức 1 khi dùng LAN hoặc nếu ổ cứng máy chủ bị nghẽn", + "ul_btn": "thả tệp / thư mục
ở đây (hoặc nhấn vào tôi)", + "ul_btnu": "T Ả I L Ê N", + "ul_btns": "T Ì M K I Ế M", + + "ul_hash": "hash", + "ul_send": "gửi", + "ul_done": "hoàn tất", + "ul_idle1": "chưa có mục nào trong hàng chờ tải lên", + "ut_etah": "tốc độ <em>hash</em> trung bình và thời gian dự kiến để hoàn tất", + "ut_etau": "tốc độ <em>tải lên</em> trung bình và thời gian dự kiến để hoàn tất", + "ut_etat": "tốc độ <em>tổng</em> trung bình và thời gian dự kiến để hoàn tất", + + "uct_ok": "hoàn tất thành công", + "uct_ng": "không hợp lệ: lỗi / bị từ chối / không tìm thấy", + "uct_done": "đã xử lý: gồm cả thành công và không hợp lệ", + "uct_bz": "đang hash hoặc tải lên", + "uct_q": "nhàn rỗi, đang chờ", + + "utl_name": "tên tệp", + "utl_ulist": "danh sách", + "utl_ucopy": "sao chép", + "utl_links": "đường dẫn", + "utl_stat": "trạng thái", + "utl_prog": "tiến trình", + + // keep short: + + // phần up2k + "utl_404": "404", + "utl_err": "LỖI", + "utl_oserr": "Lỗi hệ thống", + "utl_found": "tìm thấy", + "utl_defer": "hoãn", + "utl_yolo": "YOLO", + "utl_done": "hoàn tất", + + "ul_flagblk": "tệp đã được thêm vào hàng chờ
tuy vậy đang có một tiến trình up2k đang chạy ở một tab khác
vui lòng đợi cho đến khi tiến trình đó hoàn tất hoặc bị hủy", + "ul_btnlk": "cài đặt của máy chủ đã khóa tùy chọn ở trạng thái này", + + + "udt_up": "Tải lên", + "udt_srch": "Tìm kiếm", + "udt_drop": "thả vào đây", + + + + "u_nav_m": '
chọn phương thức tải lên
Enter = Tệp (một hoặc nhiều)\nESC = Một thư mục (kèm thư mục con)', + "u_nav_b": 'TệpMột thư mục', + + // settings / config: + "cl_opts": "tuỳ chọn", + "cl_hfsz": "kích thước tệp", + "cl_themes": "giao diện", + "cl_langs": "ngôn ngữ", + "cl_ziptype": "định dạng nén", + "cl_uopts": "tuỳ chọn up2k", + "cl_favico": "favicon", + "cl_bigdir": "thư mục lớn", + "cl_hsort": "#sắp xếp", + "cl_keytype": "ghi chú bàn phím", + "cl_hiddenc": "cột đã ẩn", + "cl_hidec": "ẩn", + "cl_reset": "đặt lại", + "cl_hpick": "chạm vào tiêu đề cột để ẩn trong bảng bên dưới", + "cl_hcancel": "đã hủy việc ẩn cột", + + // settings / tuỳ chọn + "ct_grid": '田 chế độ lưới', + "ct_ttips": '༼ ◕_◕ ༽">ℹ️ tooltips', + "ct_thumb": 'ở chế độ lưới, chuyển biểu tượng hoặc hình thu nhỏ$NPhím tắt: T">🖼️ ảnh thu nhỏ', + "ct_csel": 'dùng CTRL và SHIFT để chọn tệp trong chế độ lưới">sel', + "ct_dl": 'cưỡng chế tải xuống (không hiện thị trong dòng) khi nhấp vào tệp">dl', + "ct_ihop": 'khi đóng trình xem ảnh, cuộn xuống tệp đã xem gần nhất">g⮯', + "ct_dots": 'hiển thị tệp ẩn (nếu máy chủ cho phép)">dotfiles', + "ct_qdel": 'khi xóa tệp, chỉ hỏi xác nhận một lần">qdel', + "ct_dir1st": 'sắp xếp thư mục trước tệp">📁 first', + "ct_nsort": 'sắp xếp tự nhiên (cho tên tệp có số ở đầu)">nsort', + "ct_utc": 'hiển thị mọi thời gian theo UTC">UTC', + "ct_readme": 'hiển thị README.md trong danh sách thư mục">📜 readme', + "ct_idxh": 'hiển thị index.html thay cho danh sách thư mục">htm', + "ct_sbars": 'hiển thị thanh cuộn">⟊', + + // tuỳ chọn up2k + "cut_umod": "nếu tệp đã tồn tại trên máy chủ, cập nhật dấu thời gian chỉnh sửa cuối của máy chủ cho khớp với tệp cục bộ của bạn (yêu cầu quyền ghi và xóa)\">re📅", + + "cut_turbo": "nút YOLO, bạn gần như KHÔNG nên bật tuỳ chọn này:$N$Ndùng khi bạn đang tải lên một lượng tệp rất lớn và phải khởi động lại vì lý do nào đó, và muốn tiếp tục tải càng sớm sàng tốt$N$Ntuỳ chọn này thay thế kiểm tra hash bằng kiểm tra "kích thước tệp ở trên máy chủ có giống nhau không" nên nếu nội dung tệp khác nhau thì sẽ KHÔNG được tải lên$N$Nbạn nên tắt tuỳ chọn này sau khi tải lên xong, và "tải lên" lại các tệp đó để xác minh\">turbo", + + "cut_datechk": "không có tác dụng trừ khi nút turbo được bật$N$Ngiảm mức độ yolo một chút; kiểm tra xem dấu thời gian tệp trên máy chủ có khớp với của bạn không$N$Nnên về lý thuyết có thể phát hiện phần lớn tệp chưa xong hoặc bị lỗi, nhưng không thể thay thế cho việc chạy xác minh sau khi tắt turbo\">date-chk", + + "cut_u2sz": "kích thước (tính theo MiB) của mỗi khối tải lên; dùng kích thước khối lớn khi truyền ở khoảng cách lục địa. Hãy thử giá trị nhỏ hơn với kết nối không ổn định", + + "cut_flag": "đảm bảo chỉ một tab được tải lên tại một thời điểm $N -- tab khác cũng cần bật tùy chọn này $N -- tác dụng trong các tab cùng tên miền", + + "cut_az": "tải tệp theo thứ tự bảng chữ cái thay vì tệp nhỏ trước$N$Ntải lên theo thứ tự bảng chữ cái giúp dễ quan sát nếu có vấn đề trên máy chủ, nhưng làm tốc độ tải chậm hơn trên mạng cáp quang hoặc LAN", + + "cut_nag": "thông báo của hệ điều hành khi tải lên hoàn tất$N(chỉ khi trình duyệt hoặc tab không hoạt động)", + "cut_sfx": "âm báo khi tải lên hoàn tất$N(chỉ khi trình duyệt hoặc tab không hoạt động)", + + "cut_mt": "dùng đa luồng để tăng tốc hash tệp$N$dùng web workers và cần $nhiều RAM hơn (tối đa thêm 512 MiB)$N$làm cho https nhanh hơn 30%, http nhanh hơn 4.5 lần\">mt", + + "cut_wasm": "dùng wasm thay vì bộ hash tích hợp của trình duyệt; nhanh hơn trên trình duyệt chromium nhưng làm tăng tải CPU, và nhiều bản chrome cũ có lỗi khiến trình duyệt dùng hết RAM và treo nếu bật tuỳ chọn này\">wasm", + + // favicon + "cft_text": "chuỗi favicon (để trống và làm mới trang để tắt)", + "cft_fg": "màu chữ", + "cft_bg": "màu nền", + + // big dirs + "cdt_lim": "số tệp tối đa hiển thị trong thư mục", + "cdt_ask": "khi cuộn xuống cuối,$Nthay vì tải thêm tệp,$Nhỏi người dùng muốn làm gì", + "cdt_hsort": "số lượng luật sắp xếp(<code>,sorthref</code>) được đưa vào URL media. Đặt bằng 0 cũng sẽ bỏ qua các quy tắc sắp xếp trong liên kết media khi nhấp vào chúng", + + "tt_entree": "hiển thị thanh điều hướng (cây thư mục)$NPhím tắt: B", + "tt_detree": "hiển thị đường dẫn$NPhím tắt: B", + "tt_visdir": "cuộn đến thư mục đã chọn", + "tt_ftree": "chuyển đổi cây thư mục / tệp văn bản$NPhím tắt: V", + "tt_pdock": "hiển thị thư mục cha trong thanh ghim trên cùng", + "tt_dynt": "tự mở rộng khi cây mở rộng", + "tt_wrap": "ngắt dòng", + "tt_hover": "hiện thị dòng tràn khi rê chuột$N( không cuộn được nếu $N  con trỏ chuột nằm ngoài cột trái )", + + "ml_pmode": "ở cuối thư mục...", + "ml_btns": "lệnh", + "ml_tcode": "mã hoá lại", + // chắc là phần nhạc + "ml_tcode2": "mã hoá lại thành", + "ml_tint": "tô màu", + "ml_eq": "bộ cân bằng âm thanh", + "ml_drc": "bộ nén dải động", + + // nhạc + + "mt_loop": "lặp lại một bài\">🔁", + "mt_one": "dừng sau một bài\">1️⃣", + "mt_shuf": "trộn các bài trong thư mụcr\">🔀", + "mt_aplay": "tự động phát nếu có ID bài trong link bạn nhấp để truy cập máy chủ$N$Ntắt tuỳ chọn sẽ ngăn URL của trang cập nhật theo ID bài khi phát nhạc, tránh tự động phát nếu cài đặt mất nhưng URL còn\">a▶", + "mt_preload": "bắt đầu tải bài hát tiếp theo khi gần hết bài để phát liền mạch\">preload", + "mt_prescan": "chuyển đến thư mục tiếp theo trước khi bài cuối cùng $Nkết thúc, giúp giữ trình duyệt hoạt động $N và không dừng phát nhạc\">nav", + "mt_fullpre": "cố gắng tải trước toàn bộ bài;$N✅ bật với kết nối không ổn định,$N❌ tắt với kết nối chậm\">full", + "mt_fau": "trên điện thoại, ngăn nhạc dừng nếu bài tiếp theo tải chậm (có thể gây lỗi hiển thị tag nhạc)\">☕️", + "mt_waves": "thanh tiến trình bài hát dạng sóng:$Nhiển thị biên độ âm thanh trong thanh tiến trình\">~s", + "mt_npclip": "hiển thị nút để sao chép bài đang phát\">/np", + "mt_m3u_c": "hiển thị nút để sao chép $Nnhững bài đã chọn dưới dạng danh sách phát m3u8\">📻", + "mt_octl": "tích hợp hệ điều hành (phím tắt media / OSD)\">os-ctl", + "mt_oseek": "cho phép tìm kiếm qua tích hợp hệ điều hành$N$Nlưu ý: trên một số thiết bị (iphone), $Nthao tác này sẽ thay thế nút bài hát tiếp theo\">seek", + "mt_oscv": "hiển thị bài album trên OSD\">art", + "mt_follow": "giữ bài đang phát trong tầm nhìn\">🎯", + "mt_compact": "giao diện điều khiển thu gọn\">⟎", + "mt_uncache": "xoá bộ nhớ đệm  (thử nếu trình duyệt lưu trữ đệm $Nmột bản nhạc bị lỗi và không thể phát)\">uncache", + "mt_mloop": "lặp trong thư mục đang mở\">🔁 loop", + "mt_mnext": "tải thư mục tiếp theo và tiếp tục\">📂 next", + "mt_mstop": "dừng phát\">⏸ stop", + "mt_cflac": "chuyển flac / wav sang {0}\">flac", + "mt_caac": "chuyển aac / m4a sang {0}\">aac", + "mt_coth": "chuyển mọi loại khác (trừ mp3) thành {0}\">oth", + "mt_c2opus": "lựa chọn tốt nhất cho máy tính, laptop, android\">opus", + "mt_c2owa": "opus-weba, cho iOS 17.5 trở lên\">owa", + "mt_c2caf": "opus-caf, cho iOS 11 đến 17\">caf", + "mt_c2mp3": "dùng trên thiết bị cũ\">mp3", + "mt_c2flac": "chất lượng âm thanh tốt nhất, nhưng tệp tải xuống lớn\">flac", + "mt_c2wav": "phát không nén (tệp còn lớn hơn nữa)\">wav", + "mt_c2ok": "lựa chọn hợp lý", + "mt_c2nd": "không phải định dạng khuyến nghị cho thiết bị, nhưng hãy thử nếu bạn muốn", + "mt_c2ng": "thiết bị dường như không hỗ trợ định dạng này, nhưng hãy thử nếu bạn muốn", + "mt_xowa": "có một vài lỗi trên iOS ngăn phát nền với định dạng này; vui lòng dùng caf hoặc mp3", + "mt_tint": "mức nền (0-100) trên thanh tiến trình", + + "mt_eq": "bật bộ cân bằng âm thanh và bộ tăng ích;$N$Nboost <code>0  </code> = âm lượng chuẩn 100% (không chỉnh)$N$Nwidth <code>1  </code> = stereo chuẩn (không chỉnh)$Nwidth <code>0.5</code> = 50% pha trái-phải$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = loại bỏ lời hát :^)$N$Nbật EQ giúp cho album được phát liền mạch không ngắt quãng, nên giữ các giá trị bằng 0 (trừ width = 1) nếu bạn không muốn thay đổi âm thanh gốc", + + "mt_drc": "bật bộ nén dải động (làm phẳng âm lượng / brickwaller); cũng bật EQ để cân bằng, nên đặt tất cả EQ trừ 'width' = 0 nếu không muốn$N$Ngiảm âm thanh trên THRESHOLD dB; với mỗi RATIO dB vượt THRESHOLD thì có 1 dB đầu ra, ví dụ tresh -24 và ratio 12 => âm lượng không vượt -22 dB, có thể tăng EQ boost lên 0.8 hoặc 1.8 với ATK 0 và RLS lớn 90 (chỉ Firefox; RLS max 1 trên browser khác)$N$NXem Wikipedia để hiểu chi tiết hơn", + + "mb_play": "phát", + "mm_hashplay": "phát bản nhạc này?", + "mm_m3u": "bấm Enter/OK để phát\nbấm ESC/Cancel để chỉnh sửa", + "mp_breq": "cần firefox 82+ hoặc chrome 73+ hoặc iOS 15+", + "mm_bload": "đang tải...", + "mm_bconv": "đang chuyển đổi sang {0}, vui lòng chờ...", + "mm_opusen": "trình duyệt không hỗ trợ tệp aac / m4a;\nchuyển sang định dạng opus hiện đã được bật", + "mm_playerr": "phát lỗi: ", + "mm_eabrt": "Việc phát nhạc đã bị huỷ", + "mm_enet": "Kết nối Internet không ổn định", + "mm_edec": "Tệp này dường như đã bị hỏng??", + "mm_esupp": "Trình duyệt của bạn không nhận dạng được định dạng tệp này.", + "mm_eunk": "Lỗi không xác định", + "mm_e404": "Không thể phát âm thanh; lỗi 404: Không tìm thấy tệp.", + "mm_e403": "Không thể phát âm thanh; lỗi 403: Từ chối truy cập.\n\nThử nhấn F5 để tải lại, có thể bạn đã đăng xuất", + "mm_e500": "Không thể phát âm thanh; lỗi 500: Kiểm tra nhật ký máy chủ.", + "mm_e5xx": "Không thể phát âm thanh; lỗi máy chủ ", + "mm_nof": "không tìm thấy thêm tệp âm thanh nào gần đó", + "mm_prescan": "Đang tìm bài nhạc tiếp theo để phát...", + "mm_scank": "Đã tìm thấy bài nhạc tiếp theo:", + "mm_uncache": "đã xoá bộ nhớ đệm; tất cả bài nhạc sẽ được tải lại khi phát tiếp", + "mm_hnf": "bài nhạc này không còn tồn tại nữa", + "im_hnf": "hình ảnh này không còn tồn tại nữa", + + "f_empty": 'thư mục này trống', + "f_chide": 'ẩn cột «{0}»\n\bạn có thế hiện lại nó trong tuỳ chọn cấu hình', + "f_bigtxt": "tệp này nặng {0} MiB -- xác nhận xem dưới dạng văn bản?", + "f_bigtxt2": "chỉ xem phần cuối của tệp? điều này cũng sẽ bật theo dõi, hiển thị các dòng văn bản mới được thêm vào theo thời gian thực", + "fbd_more": '
hiện {0} của {1} tệp; hiện {2} hoặc hiện tất cả
', + "fbd_all": '
đang hiện {0} của {1} tệp; hiện tất cả
', + "f_anota": "chỉ {0} trong {1} tệp được chọn;\nđể chọn toàn bộ thư mục, trước tiên hãy kéo xuống cuối", + + "f_dls": 'những đường dẫn đến tệp trong thư mục này\nđã được chuyển thành đường dẫn tải trực tiếp', + + "f_partial": "Để tải an toàn một tệp đang được tải lên, hãy bấm vào tệp có cùng tên nhưng *không* có phần mở rộng .PARTIAL. Hãy nhấn CANCEL hoặc Escape để thực hiện.\n\nNếu nhấn OK / Enter, cảnh báo sẽ bị bỏ qua và bạn sẽ tải tệp tạm .PARTIAL thay vào đó, gần như chắc chắn dẫn đến dữ liệu bị hỏng.", + + "ft_paste": "dán {0} mục$NPhím tắt: ctrl-V", + "fr_eperm": "không thể đổi tên:\nbạn không có quyền “move” trong thư mục này", + "fd_eperm": "không thể xóa:\nbạn không có quyền “delete” trong thư mục này", + "fc_eperm": "không thể cắt:\nbạn không có quyền “move” trong thư mục này", + "fp_eperm": "không thể dán:\nbạn không có quyền “write” trong thư mục này", + "fr_emore": "hãy chọn ít nhất một mục để đổi tên", + "fd_emore": "hãy chọn ít nhất một mục để xóa", + "fc_emore": "hãy chọn ít nhất một mục để cắt", + "fcp_emore": "hãy chọn ít nhất một mục để sao chép vào bảng nhớ tạm", + + + "fs_sc": "chia sẻ thư mục hiện tại", + "fs_ss": "chia sẻ các tệp đã chọn", + "fs_just1d": "bạn không thể chọn nhiều hơn một thư mục,\nhoặc trộn tệp và thư mục trong cùng một lựa chọn", + "fs_abrt": "❌ hủy", + "fs_rand": "🎲 tên ngẫu nhiên", + "fs_go": "✅ tạo liên kết chia sẻ", + "fs_name": "tên", + "fs_src": "nguồn", + "fs_pwd": "mật khẩu", + "fs_exp": "hết hạn", + "fs_tmin": "phút", + "fs_thrs": "giờ", + "fs_tdays": "ngày", + "fs_never": "vĩnh viễn", + "fs_pname": "tên liên kết tùy chọn; sẽ dùng tên ngẫu nhiên nếu để trống", + "fs_tsrc": "tệp hoặc thư mục cần chia sẻ", + "fs_ppwd": "mật khẩu tùy chọn", + "fs_w8": "đang tạo liên kết chia sẻ...", + "fs_ok": "nhấn Enter/OK để chép vào bảng nhớ tạm\nnhấn ESC/Cancel để đóng", + + "frt_dec": "có thể sửa một số trường hợp tên tệp bị lỗi\">url-decode", + "frt_rst": "khôi phục tên gốc\">↺ reset", + "frt_abrt": "hủy và đóng cửa sổ này\">❌ cancel", + "frb_apply": "ÁP DỤNG ĐỔI TÊN", + "fr_adv": "đổi tên theo lô / metadata / pattern\">advanced", + "fr_case": "regex phân biệt hoa thường\">case", + "fr_win": "tên tương thích Windows; thay <>:"\\|?* bằng ký tự fullwidth tiếng Nhật\">win", + "fr_slash": "thay / bằng ký tự khác để tránh tạo thư mục mới\">no /", + "fr_re": "regex áp dụng lên tên gốc; các nhóm bắt có thể được tham chiếu trong trường định dạng bên dưới như <code>(1)</code>, <code>(2)</code> ...", + "fr_fmt": "lấy cảm hứng từ foobar2000:$N<code>(title)</code> được thay bằng tên bài hát,$N<code>[(artist) - ](title)</code> bỏ qua phần trong ngoặc nếu artist trống,$N<code>$lpad((tn),2,0)</code> thêm số 0 để tracknumber đủ 2 chữ số", + "fr_pdel": "xóa", + "fr_pnew": "lưu dưới tên mới", + "fr_pname": "nhập tên cho preset mới", + "fr_aborted": "đã hủy", + "fr_lold": "tên cũ", + "fr_lnew": "tên mới", + "fr_tags": "tag của các tệp đã chọn (chỉ xem, không chỉnh sửa):", + "fr_busy": "đang đổi tên {0} mục...\n\n{1}", + "fr_efail": "đổi tên thất bại:\n", + "fr_nchg": "{0} tên mới đã bị chỉnh sửa do win và/hoặc no /\n\nTiếp tục với các tên đã chỉnh sửa?", + + + "fd_ok": "hoàn tất xoá", + "fd_err": "xoá gặp lỗi:\n", + "fd_none": "không xóa được mục nào; có thể bị chặn bởi cấu hình máy chủ (xbd)?", + "fd_busy": "đang xóa {0} mục...\n\n{1}", + "fd_warn1": "XÓA {0} mục này?", + "fd_warn2": "Cảnh báo cuối! Không thể hoàn tác. Xóa?", + + "fc_ok": "đã cắt {0} mục", + "fc_warn": "đã cắt {0} mục\n\nnhưng: chỉ tab trình duyệt này có thể dán\n(vì lựa chọn quá lớn)", + + "fcc_ok": "đã sao chép {0} mục vào bảng nhớ tạm", + "fcc_warn": "đã sao chép {0} mục vào bảng nhớ tạm\n\nnhưng: chỉ tab trình duyệt này có thể dán\n(vì lựa chọn quá lớn)", + + "fp_apply": "dùng các tên này", + "fp_ecut": "hãy cắt hoặc sao chép một số tệp / thư mục trước khi dán / di chuyển\n\nlưu ý: bạn có thể cắt / dán giữa các tab trình duyệt khác nhau", + "fp_ename": "{0} mục không thể được di chuyển vào đây vì tên đã tồn tại. Hãy đặt tên mới bên dưới để tiếp tục, hoặc để trống để bỏ qua:", + "fcp_ename": "{0} mục không thể được sao chép vào đây vì tên đã tồn tại. Hãy đặt tên mới bên dưới để tiếp tục, hoặc để trống để bỏ qua:", + "fp_emore": "vẫn còn xung đột tên tệp cần xử lý", + "fp_ok": "di chuyển OK", + "fcp_ok": "sao chép OK", + "fp_busy": "đang di chuyển {0} mục...\n\n{1}", + "fcp_busy": "đang sao chép {0} mục...\n\n{1}", + "fp_abrt": "đang hủy...", + "fp_err": "di chuyển thất bại:\n", + "fcp_err": "sao chép thất bại:\n", + "fp_confirm": "di chuyển {0} mục này vào đây?", + "fcp_confirm": "sao chép {0} mục này vào đây?", + "fp_etab": "không đọc được bảng nhớ tạm từ tab trình duyệt khác", + "fp_name": "đang tải lên tệp từ thiết bị. Hãy đặt tên cho tệp:", + "fp_both_m": "
chọn thao tác để dán
Enter = Di chuyển {0} tệp từ «{1}»\nESC = Tải lên {2} tệp từ thiết bị", + "fcp_both_m": "
chọn thao tác để dán
Enter = Sao chép {0} tệp từ «{1}»\nESC = Tải lên {2} tệp từ thiết bị", + "fp_both_b": "Di chuyểnTải lên", + "fcp_both_b": "Sao chépTải lên", + + "mk_noname": "hãy nhập tên vào ô bên trái trước khi thực hiện :p", + "nmd_i1": "hãy thêm cả phần mở rộng tệp bạn muốn, ví dụ .md", + "nmd_i2": "bạn chỉ có thể tạo tệp .md vì bạn không có quyền xóa", + + + "tv_load": "Đang tải tài liệu văn bản:\n\n{0}\n\n{1}% ({2} / {3} MiB)", + "tv_xe1": "không thể tải tệp văn bản:\n\nlỗi ", + "tv_xe2": "404, không tìm thấy tệp", + "tv_lst": "danh sách các tệp văn bản trong", + "tvt_close": "quay lại chế độ xem thư mục$NPhím tắt: M (hoặc Esc)\">❌ close", + "tvt_dl": "tải xuống tệp này$NPhím tắt: Y\">💾 download", + "tvt_prev": "hiển thị tài liệu trước đó$NPhím tắt: I\">⬆ prev", + "tvt_next": "hiển thị tài liệu kế tiếp$NPhím tắt: K\">⬇ next", + "tvt_sel": "chọn tệp   (để cắt / sao chép / xóa / ...)$NPhím tắt: S\">sel", + "tvt_j": "chuẩn hóa json$NPhím tắt: shift-J\">j", + "tvt_edit": "mở tệp trong trình soạn thảo văn bản$NPhím tắt: E\">✏️ edit", + "tvt_tail": "theo dõi thay đổi của tệp; hiển thị dòng mới theo thời gian thực\">📡 follow", + "tvt_wrap": "ngắt dòng\">↵", + "tvt_atail": "khóa cuộn ở cuối trang\">⚓", + "tvt_ctail": "giải mã màu terminal (ansi escape codes)\">🌈", + "tvt_ntail": "giới hạn scrollback (số byte văn bản được giữ trong bộ nhớ)", + + + + + "m3u_add1": "đã thêm 1 bài vào danh sách phát m3u", + "m3u_addn": "đã thêm {0} bài vào danh sách phát m3u", + "m3u_clip": "danh sách phát m3u đã được chép vào bảng nhớ tạm\n\nbạn nên tạo một tệp văn bản mới tên bất kỳ.m3u rồi dán nội dung danh sách phát vào đó để có thể phát được", + + "gt_vau": "không hiện video, chỉ phát âm thanh\">🎧", + "gt_msel": "bật chọn nhiều; ctrl-click để ghi đè$N$N<em>khi bật: nhấn đúp để mở tệp / thư mục</em>$N$NPhím tắt: S\">multiselect", + "gt_crop": "cắt chính giữa ảnh\">crop", + + "gt_3x": "ảnh độ nét cao\">3x", + "gt_zoom": "zoom", + "gt_chop": "chop", + "gt_sort": "sắp xếp theo", + "gt_name": "tên", + "gt_sz": "dung lượng", + "gt_ts": "ngày", + "gt_ext": "loại", + "gt_c1": "rút ngắn tên tệp hơn (hiện ít hơn)", + "gt_c2": "rút ngắn tên tệp ít hơn (hiện nhiều hơn)", + + "sm_w8": "đang tìm...", + "sm_prev": "kết quả bên dưới là từ lần tìm trước:\n ", + "sl_close": "đóng kết quả tìm kiếm", + "sl_hits": "hiển thị {0} kết quả", + "sl_moar": "tải thêm", + + "s_sz": "dung lượng", + "s_dt": "ngày", + "s_rd": "đường dẫn", + "s_fn": "tên", + "s_ta": "tag", + "s_ua": "up@", + "s_ad": "nâng cao", + "s_s1": "tối thiểu MiB", + "s_s2": "tối đa MiB", + "s_d1": "ngày tối thiểu (iso8601)", + "s_d2": "ngày tối đa (iso8601)", + "s_u1": "tải lên sau", + "s_u2": "và/hoặc trước", + "s_r1": "đường dẫn chứa   (cách nhau bằng dấu cách)", + "s_f1": "tên chứa   (thêm -nope để phủ định)", + "s_t1": "tag chứa   (^=bắt đầu, kết thúc=$)", + "s_a1": "thuộc tính metadata cụ thể", + + + "md_eshow": "không thể tải", + "md_off": "[📜readme] đã tắt trong [⚙️] -- tài liệu bị ẩn", + + "badreply": "Không thể phân tích phản hồi từ máy chủ", + + "xhr403": "403: Access denied\n\nhãy thử nhấn F5, có thể bạn đã bị đăng xuất", + "xhr0": "không rõ (có thể mất kết nối với máy chủ hoặc máy chủ đang offline)", + "cf_ok": "rất tiếc, lớp bảo vệ DD" + wah + "oS đã kích hoạt\n\nmọi thứ sẽ tiếp tục sau khoảng 30 giây\n\nnếu không có gì xảy ra, hãy nhấn F5 để tải lại trang", + "tl_xe1": "không thể liệt kê thư mục con:\n\nlỗi ", + "tl_xe2": "404: Không tìm thấy thư mục", + "fl_xe1": "không thể liệt kê tệp trong thư mục:\n\nlỗi ", + "fl_xe2": "404: Không tìm thấy thư mục", + "fd_xe1": "không thể tạo thư mục con:\n\nlỗi ", + "fd_xe2": "404: Không tìm thấy thư mục cha", + "fsm_xe1": "không thể gửi tin nhắn:\n\nlỗi ", + "fsm_xe2": "404: Không tìm thấy thư mục cha", + "fu_xe1": "không tải được danh sách unpost từ máy chủ:\n\nlỗi ", + "fu_xe2": "404: Không tìm thấy tệp??", + + "fz_tar": "file gnu-tar không nén (linux / mac)", + "fz_pax": "file tar định dạng pax không nén (chậm hơn)", + "fz_targz": "gnu-tar với nén gzip mức 3$N$Nthường rất chậm nên$Nhãy dùng tar không nén", + "fz_tarxz": "gnu-tar với nén xz mức 1$N$Nthường rất chậm nên$Nhãy dùng tar không nén", + "fz_zip8": "zip với tên tệp utf8 (có thể lỗi trên windows 7 và cũ hơn)", + "fz_zipd": "zip với tên tệp cp437 truyền thống, dành cho phần mềm rất cũ", + "fz_zipc": "cp437 với crc32 tính sớm,$Ndành cho MS-DOS PKZIP v2.04g (tháng 10/1993)$N(mất thời gian xử lý lâu hơn trước khi tải xuống bắt đầu)", + + "un_m1": "bạn có thể xóa các lần upload gần đây (hoặc hủy những mục chưa hoàn tất) bên dưới", + "un_upd": "làm mới", + "un_m4": "hoặc chia sẻ các tệp hiển thị bên dưới:", + "un_ulist": "hiện", + "un_ucopy": "chép", + "un_flt": "bộ lọc tùy chọn:  đường dẫn phải chứa", + "un_fclr": "xóa bộ lọc", + "un_derr": "xóa unpost thất bại:\n", + "un_f5": "có gì đó lỗi rồi, hãy thử tải lại trang hoặc nhấn F5", + "un_uf5": "xin lỗi nhưng bạn cần tải lại trang (ví dụ nhấn F5 hoặc CTRL-R) trước khi có thể hủy upload này", + "un_nou": "cảnh báo: máy chủ đang bận nên không thể hiển thị các upload chưa hoàn tất; hãy bấm “làm mới” sau một lúc", + "un_noc": "cảnh báo: unpost cho các tệp đã upload xong không được bật hoặc không được phép trong cấu hình máy chủ", + "un_max": "đang hiển thị 2000 tệp đầu tiên (hãy dùng bộ lọc)", + "un_avail": "có thể xóa {0} tải lên gần đây
{1} mục chưa hoàn tất có thể hủy", + "un_m2": "sắp xếp theo thời gian tải lên; mới nhất ở trên:", + "un_no1": "không có tải lên nào đủ mới", + "un_no2": "không có tải lên nào đủ mới khớp với bộ lọc đó", + "un_next": "xóa {0} tệp kế bên dưới", + "un_abrt": "hủy", + "un_del": "xóa", + "un_m3": "đang tải danh sách tải lên gần đây...", + "un_busy": "đang xóa {0} tệp...", + "un_clip": "{0} liên kết đã chép vào bảng nhớ tạm", + + + "u_https1": "bạn nên", + "u_https2": "chuyển sang https", + "u_https3": "để có hiệu suất tốt hơn", + + "u_ancient": "trình duyệt của bạn quá cũ; bạn có thể dùng bup thay thế", + "u_nowork": "cần Firefox 53+, Chrome 57+ hoặc iOS 11+", + "tail_2old": "cần Firefox 105+, Chrome 71+ hoặc iOS 14.5+", + "u_nodrop": "trình duyệt của bạn quá cũ để dùng kéo thả khi tải lên", + "u_notdir": "đây không phải thư mục\n\ntrình duyệt của bạn quá cũ,\nvui lòng thử dùng dragdrop", + + "u_uri": "để kéo thả ảnh từ cửa sổ trình duyệt khác,\nvui lòng thả lên nút tải lên lớn", + + "u_enpot": "chuyển sang giao diện đơn giản (có thể tăng tốc độ tải lên)", + "u_depot": "chuyển sang giao diện đầy đủ (có thể giảm tốc độ tải lên)", + "u_gotpot": "đang chuyển sang giao diện đơn giản để cải thiện tốc độ tải lên\n\nbạn có thể đổi lại bất kỳ lúc nào", + + "u_pott": "

tệp:   {0} hoàn tất,   {1} lỗi,   {2} đang chạy,   {3} chờ

", + + "u_ever": "đây là trình tải lên cơ bản; up2k yêu cầu
Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1 trở lên", + "u_su2k": "đây là trình tải lên cơ bản; up2k tốt hơn", + + "u_uput": "tối ưu tốc độ (bỏ qua checksum)", + "u_ewrite": "bạn không có quyền ghi vào thư mục này", + "u_eread": "bạn không có quyền đọc thư mục này", + "u_enoi": "tìm kiếm tệp chưa được bật trong cấu hình máy chủ", + "u_enoow": "ghi đè không khả dụng; cần quyền Xóa", + + "u_badf": "Đã bỏ qua {0} tệp (trong tổng {1}) có thể do quyền hệ thống tệp:\n\n", + "u_blankf": "{0} tệp (trong tổng {1}) trống; vẫn tải lên?\n\n", + "u_applef": "{0} tệp (trong tổng {1}) có thể không cần thiết;\nNhấn OK/Enter để BỎ QUA,\nNhấn Cancel/ESC để giữ lại và tải lên:\n\n", + + "u_just1": "\nCó thể sẽ hoạt động tốt hơn nếu bạn chỉ chọn một tệp", + "u_ff_many": "nếu bạn dùng Linux / macOS / Android thì số lượng tệp này có thể làm Firefox crash\nnếu xảy ra, vui lòng thử lại hoặc dùng Chrome", + + "u_up_life": "Tệp tải lên sẽ bị xóa khỏi máy chủ\n{0} sau khi hoàn tất", + "u_asku": "tải {0} tệp này lên {1}", + "u_unpt": "bạn có thể hoàn tác hoặc xóa lần tải lên này bằng biểu tượng ở góc trên bên trái 🧯", + + "u_bigtab": "sắp hiển thị {0} tệp\n\nviệc này có thể làm trình duyệt treo, bạn có chắc không?", + "u_scan": "Đang quét tệp...", + "u_dirstuck": "bộ lặp thư mục bị treo khi truy cập {0} mục sau; sẽ bỏ qua:", + "u_etadone": "Hoàn tất ({0}, {1} tệp)", + "u_etaprep": "(đang chuẩn bị tải lên)", + "u_hashdone": "hash hoàn tất", + "u_hashing": "hash", + "u_hs": "đang bắt tay...", + "u_started": "tệp đang được tải lên; xem biểu tượng [🚀]", + "u_dupdefer": "bị trùng; sẽ xử lý sau khi hoàn tất các tệp khác", + + "u_actx": "nhấn vào dòng này để tránh giảm hiệu suất khi chuyển cửa sổ hoặc tab", + "u_fixed": "OK;  đã sửa 👍", + + "u_cuerr": "không tải được phần {0}/{1};\ncó thể không nghiêm trọng, tiếp tục\n\ntệp: {2}", + "u_cuerr2": "máy chủ từ chối phần tải {0}/{1};\nsẽ thử lại sau\n\ntệp: {2}\n\nlỗi ", + "u_ehstmp": "sẽ thử lại; xem góc dưới bên phải", + "u_ehsfin": "máy chủ từ chối yêu cầu hoàn tất tải lên; đang thử lại...", + "u_ehssrch": "máy chủ từ chối yêu cầu tìm kiếm; đang thử lại...", + "u_ehsinit": "máy chủ từ chối yêu cầu bắt đầu tải lên; đang thử lại...", + "u_eneths": "lỗi mạng khi thực hiện bắt tay; đang thử lại...", + "u_enethd": "lỗi mạng khi kiểm tra tồn tại mục tiêu; đang thử lại...", + "u_cbusy": "chờ máy chủ cho phép tiếp tục sau sự cố mạng...", + "u_ehsdf": "máy chủ hết dung lượng!\n\nsẽ tiếp tục thử trong trường hợp có ai đó giải phóng dung lượng", + + "u_emtleak1": "có dấu hiệu cho thấy trình duyệt có thể bị rò rỉ bộ nhớ;\nvui lòng", + "u_emtleak2": " chuyển sang https (khuyến nghị) hoặc ", + "u_emtleak3": " ", + "u_emtleakc": "thử cách sau:\n
  • nhấn F5 để tải lại trang
  • sau đó tắt nút  mt  trong  ⚙️ cài đặt
  • và thử tải lại
Tốc độ có thể chậm hơn một chút\nXin lỗi vì sự bất tiện\n\nPS: chrome v107 đã có bản sửa", + "u_emtleakf": "thử cách sau:\n
  • nhấn F5 để tải lại trang
  • sau đó bật 🥔 (potato) trong giao diện tải lên
  • và thử lại
\nPS: firefox hy vọng sẽ có bản sửa", + + "u_s404": "không tìm thấy trên máy chủ", + "u_expl": "giải thích", + "u_maxconn": "đa số trình duyệt giới hạn giá trị này ở mức 6, nhưng Firefox cho phép tăng bằng connections-per-server trong about:config", + + "u_tu": "

CẢNH BÁO: turbo đang bật  client có thể không nhận diện và tiếp tục tải lại tệp chưa hoàn tất; xem tooltip nút turbo

", + "u_ts": "

CẢNH BÁO: turbo đang bật  kết quả tìm kiếm có thể không chính xác; xem tooltip nút turbo

", + + "u_turbo_c": "turbo bị tắt trong cấu hình máy chủ", + "u_turbo_g": "đang tắt turbo vì bạn không có quyền liệt kê thư mục trong phân vùng này", + + "u_life_cfg": "tự xóa sau phút (hoặc giờ)", + "u_life_est": "tệp sẽ bị xóa ---", + "u_life_max": "thư mục này áp dụng\nthời gian tồn tại tối đa {0}", + + "u_unp_ok": "cho phép unpost trong {0}", + "u_unp_ng": "không cho phép unpost", + + "ue_ro": "bạn chỉ có quyền đọc thư mục này\n\n", + "ue_nl": "bạn chưa đăng nhập", + "ue_la": "bạn đang đăng nhập với tài khoản \"{0}\"", + "ue_sr": "bạn đang ở chế độ tìm kiếm tệp\n\nhãy chuyển sang chế độ tải lên bằng cách nhấn biểu tượng kính lúp 🔎 (bên cạnh nút SEARCH), rồi thử lại\n\nxin lỗi", + "ue_ta": "hãy thử tải lên lại; giờ có thể được", + "ue_ab": "tệp này đang được tải lên ở thư mục khác và cần hoàn tất trước khi có thể tải lên nơi khác.\n\nBạn có thể hủy và quên tiến trình ban đầu bằng biểu tượng ở góc trên bên trái 🧯", + + "ur_1uo": "OK: tệp đã tải lên thành công", + "ur_auo": "OK: toàn bộ {0} tệp đã tải lên thành công", + "ur_1so": "OK: đã tìm thấy tệp trên máy chủ", + "ur_aso": "OK: đã tìm thấy toàn bộ {0} tệp trên máy chủ", + + "ur_1un": "Tải lên thất bại", + "ur_aun": "{0} lần tải lên đều thất bại", + + "ur_1sn": "KHÔNG tìm thấy tệp trên máy chủ", + "ur_asn": "{0} tệp KHÔNG tìm thấy trên máy chủ", + + "ur_um": "Hoàn tất\n{0} tải lên thành công,\n{1} tải lên thất bại", + "ur_sm": "Hoàn tất\n{0} tệp tìm thấy trên máy chủ,\n{1} tệp KHÔNG tìm thấy", + + "lang_set": "tải lại trang để áp dụng thay đổi ngôn ngữ", + + "splash": { + "a1": "tải lại", + "b1": "xin chào khách   (bạn chưa đăng nhập)", + "c1": "đăng xuất", + "d1": "ghi lại ngăn xếp", // TLNote: "d2" is the tooltip for this button + "d2": "hiển thị trạng thái của tất cả các luồng đang hoạt động", + "e1": "tải lại cấu hình", + "e2": "tải lại các tệp cấu hình (tài khoản/ổ đĩa/cờ ổ đĩa),$Nvà quét lại tất cả các ổ đĩa e2ds$N$Nchú ý: bất kỳ thay đổi nào đối với cài đặt toàn cục$Ncần khởi động lại hoàn toàn để có hiệu lực", + "f1": "bạn có thể duyệt:", + "g1": "bạn có thể tải lên:", + "cc1": "thứ khác:", + "h1": "vô hiệu hoá k304", // TLNote: "j1" explains what k304 is + "i1": "bật k304", + "j1": "bật k304 sẽ ngắt kết nối client của bạn trên mỗi HTTP 304, tùy chọn này có thể ngăn một số proxy bị lỗi kẹt (đột ngột không tải được trang), nhưng nó cũng sẽ làm mọi thứ chậm hơn", + "k1": "đặt lại cài đặt client", + "l1": "đăng nhập để có thêm:", + "m1": "chào mừng trở lại,", + "n1": "404 không tìm thấy  ┐( ´ -`)┌", + "o1": 'hoặc có thể bạn không có quyền truy cập -- thử mật khẩu hoặc về trang chủ', + "p1": "403 bị cấm  ~┻━┻", + "q1": 'sử dụng mật khẩu hoặc về trang chủ', + "r1": "về trang chủ", + ".s1": "quét lại", + "t1": "hành động", // TLNote: this is the header above the "rescan" buttons + "u2": "thời gian kể từ lần ghi máy chủ cuối cùng$N( tải lên / đổi tên / ... )$N$N17d = 17 ngày$N1h23 = 1 giờ 23 phút$N4m56 = 4 phút 56 giây", + "v1": "kết nối", + "v2": "sử dụng máy chủ này như một ổ cứng cục bộ", + "w1": "chuyển sang https", + "x1": "đổi mật khẩu", + "y1": "chỉnh sửa chia sẻ", // TLNote: shows the list of folders that the user has decided to share + "z1": "mở khóa chia sẻ này:", // TLNote: the password prompt to see a hidden share + "ta1": "điền mật khẩu mới:", + "ta2": "nhập lại mật khẩu mới:", + "ta3": "mật khẩu không khớp, xin hãy thử lại", + "aa1": "tệp đến", + "ab1": "vô hiệu hóa no304", + "ac1": "bật no304", + "ad1": "bật no304 sẽ vô hiệu hóa tất cả bộ nhớ đệm; hãy thử tùy chọn này nếu k304 không đủ. Tùy chọn này sẽ làm lãng phí một lượng lớn lưu lượng mạng!", + "ae1": "tải xuống đang hoạt động:", + "af1": "hiển thị các tệp đã tải lên gần đây", + } +}; From d4a9787c6c23c1be6a3a450763152153a568ddef Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 20:17:55 +0000 Subject: [PATCH 59/67] enable vietnamese translation --- copyparty/__init__.py | 1 + copyparty/web/browser.js | 1 + copyparty/web/tl/vie.js | 10 +++++----- scripts/sfx.ls | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 8dba6457..ed48dc50 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -118,6 +118,7 @@ web/tl/spa.js web/tl/swe.js web/tl/tur.js web/tl/ukr.js +web/tl/vie.js web/ui.css web/up2k.js web/util.js diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2d2224b3..2881c54c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -664,6 +664,7 @@ var LANGN = [ ["swe", "Svenska"], ["tur", "Türkçe"], ["ukr", "Українська"], + ["vie", "Tiếng Việt"], ]; if (window.langmod) diff --git a/copyparty/web/tl/vie.js b/copyparty/web/tl/vie.js index a11f34e6..71e91194 100644 --- a/copyparty/web/tl/vie.js +++ b/copyparty/web/tl/vie.js @@ -687,14 +687,14 @@ Ls.vie = { "a1": "tải lại", "b1": "xin chào khách   (bạn chưa đăng nhập)", "c1": "đăng xuất", - "d1": "ghi lại ngăn xếp", // TLNote: "d2" is the tooltip for this button + "d1": "ghi lại ngăn xếp", "d2": "hiển thị trạng thái của tất cả các luồng đang hoạt động", "e1": "tải lại cấu hình", "e2": "tải lại các tệp cấu hình (tài khoản/ổ đĩa/cờ ổ đĩa),$Nvà quét lại tất cả các ổ đĩa e2ds$N$Nchú ý: bất kỳ thay đổi nào đối với cài đặt toàn cục$Ncần khởi động lại hoàn toàn để có hiệu lực", "f1": "bạn có thể duyệt:", "g1": "bạn có thể tải lên:", "cc1": "thứ khác:", - "h1": "vô hiệu hoá k304", // TLNote: "j1" explains what k304 is + "h1": "vô hiệu hoá k304", "i1": "bật k304", "j1": "bật k304 sẽ ngắt kết nối client của bạn trên mỗi HTTP 304, tùy chọn này có thể ngăn một số proxy bị lỗi kẹt (đột ngột không tải được trang), nhưng nó cũng sẽ làm mọi thứ chậm hơn", "k1": "đặt lại cài đặt client", @@ -706,14 +706,14 @@ Ls.vie = { "q1": 'sử dụng mật khẩu hoặc về trang chủ', "r1": "về trang chủ", ".s1": "quét lại", - "t1": "hành động", // TLNote: this is the header above the "rescan" buttons + "t1": "hành động", "u2": "thời gian kể từ lần ghi máy chủ cuối cùng$N( tải lên / đổi tên / ... )$N$N17d = 17 ngày$N1h23 = 1 giờ 23 phút$N4m56 = 4 phút 56 giây", "v1": "kết nối", "v2": "sử dụng máy chủ này như một ổ cứng cục bộ", "w1": "chuyển sang https", "x1": "đổi mật khẩu", - "y1": "chỉnh sửa chia sẻ", // TLNote: shows the list of folders that the user has decided to share - "z1": "mở khóa chia sẻ này:", // TLNote: the password prompt to see a hidden share + "y1": "chỉnh sửa chia sẻ", + "z1": "mở khóa chia sẻ này:", "ta1": "điền mật khẩu mới:", "ta2": "nhập lại mật khẩu mới:", "ta3": "mật khẩu không khớp, xin hãy thử lại", diff --git a/scripts/sfx.ls b/scripts/sfx.ls index e387844a..73e362cf 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -132,6 +132,7 @@ copyparty/web/tl/spa.js, copyparty/web/tl/swe.js, copyparty/web/tl/tur.js, copyparty/web/tl/ukr.js, +copyparty/web/tl/vie.js, copyparty/web/ui.css, copyparty/web/up2k.js, copyparty/web/util.js, From db38cb2f79e8f2cd94dde970d955268263b806cb Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 20:27:09 +0000 Subject: [PATCH 60/67] mtl new strings --- copyparty/web/tl/chi.js | 2 ++ copyparty/web/tl/cze.js | 3 +++ copyparty/web/tl/deu.js | 3 +++ copyparty/web/tl/epo.js | 2 ++ copyparty/web/tl/fin.js | 2 ++ copyparty/web/tl/fra.js | 3 +++ copyparty/web/tl/grc.js | 3 +++ copyparty/web/tl/ita.js | 2 ++ copyparty/web/tl/kor.js | 2 ++ copyparty/web/tl/nld.js | 2 ++ copyparty/web/tl/nno.js | 2 ++ copyparty/web/tl/pol.js | 2 ++ copyparty/web/tl/por.js | 2 ++ copyparty/web/tl/rus.js | 2 ++ copyparty/web/tl/spa.js | 2 ++ copyparty/web/tl/swe.js | 2 ++ copyparty/web/tl/tur.js | 3 +++ copyparty/web/tl/ukr.js | 2 ++ copyparty/web/tl/vie.js | 3 +++ scripts/tl.js | 1 + scripts/tl.py | 1 + 21 files changed, 46 insertions(+) diff --git a/copyparty/web/tl/chi.js b/copyparty/web/tl/chi.js index 6987904d..ecaa13ed 100644 --- a/copyparty/web/tl/chi.js +++ b/copyparty/web/tl/chi.js @@ -678,6 +678,8 @@ Ls.chi = { "ta1": "请先输入新密码", "ta2": "重复以确认新密码:", "ta3": "发现拼写错误;请重试", + "nop": "错误:密码不能为空", //m + "nou": "错误:用户名和/或密码不能为空", //m "aa1": "正在接收的文件:", //m "ab1": "关闭 k304", "ac1": "开启 k304", diff --git a/copyparty/web/tl/cze.js b/copyparty/web/tl/cze.js index f0fad123..75006c9e 100644 --- a/copyparty/web/tl/cze.js +++ b/copyparty/web/tl/cze.js @@ -682,11 +682,14 @@ Ls.cze = { "ta1": "nejprve vyplňte své nové heslo", "ta2": "zopakujte pro potvrzení nového hesla:", "ta3": "nalezen překlep; zkuste to prosím znovu", + "nop": "CHYBA: Heslo nesmí být prázdné", //m + "nou": "CHYBA: Uživatelské jméno a/nebo heslo nesmí být prázdné", //m "aa1": "příchozí soubory:", "ab1": "deaktivovat no304", "ac1": "povolit no304", "ad1": "povolení no304 deaktivuje veškeré mezipaměti; zkuste to, pokud k304 nestačilo. To ovšem zapříčíní obrovské množství síťového provozu!", "ae1": "aktivní stahování:", "af1": "zobrazit nedávné nahrávání", + "ag1": "zobrazit známé uživatele IdP", //m } }; diff --git a/copyparty/web/tl/deu.js b/copyparty/web/tl/deu.js index 15efa50d..62c4d001 100644 --- a/copyparty/web/tl/deu.js +++ b/copyparty/web/tl/deu.js @@ -678,11 +678,14 @@ Ls.deu = { "ta1": "Trage zuerst dein Passwort ein", "ta2": "Wiederhole dein Passwort zur Bestätigung:", "ta3": "Da stimmt etwas nicht; probier's nochmal", + "nop": "FEHLER: Passwort darf nicht leer sein", //m + "nou": "FEHLER: Benutzername und/oder Passwort dürfen nicht leer sein", //m "aa1": "Eingehende Dateien:", "ab1": "no304 deaktivieren", "ac1": "no304 aktivieren", "ad1": "Das Aktivieren von no304 deaktiviert jegliche Form von Caching; probier dies, wenn k304 nicht genug war. Dies verschwendet eine grosse Menge Netzwerk-Traffic!", "ae1": "Aktive Downloads:", "af1": "Zeige neue Uploads", + "ag1": "Bekannte IdP-Benutzer anzeigen", //m } }; diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index 322cb9a9..0d254491 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -678,6 +678,8 @@ Ls.epo = { "ta1": "entajpu novan pasvorton unue", "ta2": "retajpu por konfirmi:", "ta3": "tajpo-eraro; bonvolu provu denove", + "nop": "ERARO: Pasvorto ne povas esti malplena", //m + "nou": "ERARO: Uzantnomo kaj/aŭ pasvorto ne povas esti malplena", //m "aa1": "aktivaj alŝutoj:", "ab1": "malŝalti no304-on", "ac1": "ŝalti no304-on", diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index 1088cc62..8e493341 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -678,6 +678,8 @@ Ls.fin = { "ta1": "täytä ensin uusi salasana", "ta2": "toista vahvistaaksesi uuden salasanan:", "ta3": "löytyi kirjoitusvirhe; yritä uudelleen", + "nop": "VIRHE: Salasana ei voi olla tyhjä", //m + "nou": "VIRHE: Käyttäjänimi ja/tai salasana ei voi olla tyhjä", //m "aa1": "saapuvat:", "ab1": "poista no304 käytöstä", "ac1": "ota no304 käyttöön", diff --git a/copyparty/web/tl/fra.js b/copyparty/web/tl/fra.js index a090a3f0..e95625ce 100644 --- a/copyparty/web/tl/fra.js +++ b/copyparty/web/tl/fra.js @@ -678,11 +678,14 @@ Ls.fra = { "ta1": "entrez d'abord votre nouveau mot de passe", "ta2": "répétez pour confirmer le nouveau mot de passe :", "ta3": "une faute de frappe a été détectée ; veuillez réessayer.", + "nop": "ERREUR : Le mot de passe ne peut pas être vide", //m + "nou": "ERREUR : Le nom d’utilisateur et/ou le mot de passe ne peut pas être vide", //m "aa1": "fichiers entrants :", "ab1": "désactiver no304", "ac1": "activer no304", "ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !", "ae1": "téléchargements actifs :", "af1": "afficher les derniers téléchargements", + "ag1": "afficher les utilisateurs IdP connus", //m } }; diff --git a/copyparty/web/tl/grc.js b/copyparty/web/tl/grc.js index 27ef5edf..c730d317 100644 --- a/copyparty/web/tl/grc.js +++ b/copyparty/web/tl/grc.js @@ -678,11 +678,14 @@ Ls.grc = { "ta1": "συμπλήρωσε πρώτα το νέο σου κωδικό", "ta2": "επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:", "ta3": "βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά", + "nop": "ΣΦΑΛΜΑ: Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός", //m + "nou": "ΣΦΑΛΜΑ: Το όνομα χρήστη και/ή ο κωδικός πρόσβασης δεν μπορεί να είναι κενό", //m "aa1": "εισερχόμενα αρχεία:", "ab1": "απενεργοποίηση no304", "ac1": "ενεργοποίηση no304", "ad1": "η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!", "ae1": "ενεργές μεταφορτώσεις:", "af1": "προβολή πρόσφατων μεταφορτώσεων", + "ag1": "εμφάνιση γνωστών χρηστών IdP", //m } }; diff --git a/copyparty/web/tl/ita.js b/copyparty/web/tl/ita.js index a65020de..465f44e3 100644 --- a/copyparty/web/tl/ita.js +++ b/copyparty/web/tl/ita.js @@ -678,6 +678,8 @@ Ls.ita = { "ta1": "devi prima inserire una nuova password", "ta2": "ripeti per confermare la nuova password:", "ta3": "errore di digitazione; riprova", + "nop": "ERRORE: La password non può essere vuota", //m + "nou": "ERRORE: Il nome utente e/o la password non possono essere vuoti", //m "aa1": "in arrivo:", "ab1": "disattiva no304", "ac1": "attiva no304", diff --git a/copyparty/web/tl/kor.js b/copyparty/web/tl/kor.js index 7321d25c..454ad9b2 100644 --- a/copyparty/web/tl/kor.js +++ b/copyparty/web/tl/kor.js @@ -678,6 +678,8 @@ Ls.kor = { "ta1": "새 비밀번호를 먼저 입력하세요", "ta2": "새 비밀번호 확인을 위해 다시 입력하세요:", "ta3": "오타가 있습니다. 다시 시도해주세요", + "nop": "오류: 비밀번호를 비워 둘 수 없습니다", //m + "nou": "오류: 사용자 이름 및/또는 비밀번호를 비워 둘 수 없습니다", //m "aa1": "수신 중인 파일:", "ab1": "no304 비활성화", "ac1": "no304 활성화", diff --git a/copyparty/web/tl/nld.js b/copyparty/web/tl/nld.js index 666bed4a..8f494479 100644 --- a/copyparty/web/tl/nld.js +++ b/copyparty/web/tl/nld.js @@ -678,6 +678,8 @@ Ls.nld = { "ta1": "Je moet eerst een nieuw wachtwoord invoeren", "ta2": "Herhaal om nieuw wachtwoord te bevestigen:", "ta3": "Typefout gevonden; probeer het opnieuw", + "nop": "FOUT: Wachtwoord mag niet leeg zijn", //m + "nou": "FOUT: Gebruikersnaam en/of wachtwoord mag niet leeg zijn", //m "aa1": "Inkomend:", "ab1": "Schakel nr. 304 uit", "ac1": "Schakel nr. 304 in", diff --git a/copyparty/web/tl/nno.js b/copyparty/web/tl/nno.js index 18ff9ac5..41829abb 100644 --- a/copyparty/web/tl/nno.js +++ b/copyparty/web/tl/nno.js @@ -675,6 +675,8 @@ Ls.nno = { "ta1": "du må skrive eit nytt passord først", "ta2": "gjenta for å stadfeste nytt passord:", "ta3": "fant ein skrivefeil; vennligst prøv igjen", + "nop": "FEIL: Passord kan ikkje vere tomt", + "nou": "FEIL: Brukarnamn og passord må fyllast ut", "aa1": "innkommande:", "ab1": "skru av no304", "ac1": "skru på no304", diff --git a/copyparty/web/tl/pol.js b/copyparty/web/tl/pol.js index 1abcea68..d564b4bb 100644 --- a/copyparty/web/tl/pol.js +++ b/copyparty/web/tl/pol.js @@ -681,6 +681,8 @@ Ls.pol = { "ta1": "najpierw wprowadź nowe hasło", "ta2": "powtórz hasło dla potwierdzenia:", "ta3": "znaleziono literówkę, spróbuj ponownie", + "nop": "BŁĄD: Hasło nie może być puste", //m + "nou": "BŁĄD: Nazwa użytkownika i/lub hasło nie może być puste", //m "aa1": "pliki przychodzące:", "ab1": "wyłącz no304", "ac1": "włącz no304", diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index bb531490..aef5cdd5 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -678,6 +678,8 @@ Ls.por = { "ta1": "primeiro digite sua nova senha", "ta2": "repita para confirmar a nova senha:", "ta3": "há um erro; por favor, tente novamente", + "nop": "ERRO: A senha não pode estar em branco", //m + "nou": "ERRO: O nome de usuário e/ou a senha não podem estar em branco", //m "aa1": "arquivos de entrada:", "ab1": "desativar no304", "ac1": "ativar no304", diff --git a/copyparty/web/tl/rus.js b/copyparty/web/tl/rus.js index 0c1a6b15..2578f15a 100644 --- a/copyparty/web/tl/rus.js +++ b/copyparty/web/tl/rus.js @@ -678,6 +678,8 @@ Ls.rus = { "ta1": "сначала введите свой новый пароль", "ta2": "повторите новый пароль:", "ta3": "опечатка; попробуйте снова", + "nop": "ОШИБКА: Пароль не может быть пустым", //m + "nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми", //m "aa1": "входящие файлы:", "ab1": "отключить no304", "ac1": "включить no304", diff --git a/copyparty/web/tl/spa.js b/copyparty/web/tl/spa.js index 2fed11b5..c2864003 100644 --- a/copyparty/web/tl/spa.js +++ b/copyparty/web/tl/spa.js @@ -677,6 +677,8 @@ Ls.spa = { "ta1": "primero escribe tu nueva contraseña", "ta2": "repite para confirmar la nueva contraseña:", "ta3": "hay un error; por favor, inténtalo de nuevo", + "nop": "ERROR: La contraseña no puede estar vacía", //m + "nou": "ERROR: El nombre de usuario y/o la contraseña no pueden estar vacíos", //m "aa1": "archivos entrantes:", "ab1": "desactivar no304", "ac1": "activar no304", diff --git a/copyparty/web/tl/swe.js b/copyparty/web/tl/swe.js index 5f3ad313..3ffd8eed 100644 --- a/copyparty/web/tl/swe.js +++ b/copyparty/web/tl/swe.js @@ -678,6 +678,8 @@ Ls.swe = { "ta1": "fyll i ditt nya lösenord", "ta2": "upprepa det nya lösenordet:", "ta3": "det blev fel; vänligen försök igen", + "nop": "FEL: Lösenordet får inte vara tomt", //m + "nou": "FEL: Användarnamn och/eller lösenord får inte vara tomt", //m "aa1": "inkommande filer:", "ab1": "avaktivera no304", "ac1": "aktivera no304", diff --git a/copyparty/web/tl/tur.js b/copyparty/web/tl/tur.js index ef45f717..2aafd3d7 100644 --- a/copyparty/web/tl/tur.js +++ b/copyparty/web/tl/tur.js @@ -673,11 +673,14 @@ Ls.tur = { "ta1": "ilk önce yeni şifreyi doldur", "ta2": "yeni şifreyi onaylamak için tekrar girin:", "ta3": "bir yazım hatası bulundu; lütfen tekrar deneyin", + "nop": "HATA: Parola boş olamaz", //m + "nou": "HATA: Kullanıcı adı ve/veya parola boş olamaz", //m "aa1": "gelen dosyalar:", "ab1": "no304'ü devre dışı bırak", "ac1": "no304'ü etkinleştir", "ad1": "no304'ü etkinleştirmek, tüm önbelleği devre dışı bırakır; bunu k304 yeterli olmadıysa deneyin. Bu, büyük miktarda ağ trafiği israf edecektir!", "ae1": "aktif indirmeler:", "af1": "son yüklemeleri göster", + "ag1": "bilinen IdP kullanıcılarını göster", //m } }; diff --git a/copyparty/web/tl/ukr.js b/copyparty/web/tl/ukr.js index 1416769a..9d336516 100644 --- a/copyparty/web/tl/ukr.js +++ b/copyparty/web/tl/ukr.js @@ -678,6 +678,8 @@ Ls.ukr = { "ta1": "спочатку заповніть ваш новий пароль", "ta2": "повторіть для підтвердження нового пароля:", "ta3": "описка; спробуйте знову", + "nop": "ПОМИЛКА: Пароль не може бути порожнім", //m + "nou": "ПОМИЛКА: Ім’я користувача та/або пароль не можуть бути порожніми", //m "aa1": "вхідні файли:", "ab1": "вимкнути no304", "ac1": "увімкнути no304", diff --git a/copyparty/web/tl/vie.js b/copyparty/web/tl/vie.js index 71e91194..32cde8b9 100644 --- a/copyparty/web/tl/vie.js +++ b/copyparty/web/tl/vie.js @@ -717,11 +717,14 @@ Ls.vie = { "ta1": "điền mật khẩu mới:", "ta2": "nhập lại mật khẩu mới:", "ta3": "mật khẩu không khớp, xin hãy thử lại", + "nop": "LỖI: Mật khẩu không được để trống", //m + "nou": "LỖI: Tên người dùng và/hoặc mật khẩu không được để trống", //m "aa1": "tệp đến", "ab1": "vô hiệu hóa no304", "ac1": "bật no304", "ad1": "bật no304 sẽ vô hiệu hóa tất cả bộ nhớ đệm; hãy thử tùy chọn này nếu k304 không đủ. Tùy chọn này sẽ làm lãng phí một lượng lớn lưu lượng mạng!", "ae1": "tải xuống đang hoạt động:", "af1": "hiển thị các tệp đã tải lên gần đây", + "ag1": "hiển thị người dùng IdP đã biết", //m } }; diff --git a/scripts/tl.js b/scripts/tl.js index 28146c09..39b6542c 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -710,5 +710,6 @@ Ls.hmn = { "ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!", "ae1": "active downloads:", "af1": "show recent uploads", + "ag1": "view idp cache", // TLNote: is a link to a page where IdP users can be managed } }; diff --git a/scripts/tl.py b/scripts/tl.py index 52c1c54f..39893445 100755 --- a/scripts/tl.py +++ b/scripts/tl.py @@ -113,6 +113,7 @@ Ls.{lang3} = {{ "ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!", "ae1": "active downloads:", "af1": "show recent uploads", + "ag1": "view idp cache", // TLNote: is a link to a page where IdP users can be managed }} }}; """ From d12cf9aee1707ef0667cfa36f33fd64233e35833 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 20:27:55 +0000 Subject: [PATCH 61/67] u2c: fix deprecation + add -teh --- bin/partyfuse.py | 9 +++++---- bin/u2c.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/partyfuse.py b/bin/partyfuse.py index 0d88caca..7be355e1 100755 --- a/bin/partyfuse.py +++ b/bin/partyfuse.py @@ -6,8 +6,8 @@ __copyright__ = 2019 __license__ = "MIT" __url__ = "https://github.com/9001/copyparty/" -S_VERSION = "2.1" -S_BUILD_DT = "2025-09-06" +S_VERSION = "2.2" +S_BUILD_DT = "2025-12-16" """ mount a copyparty server (local or remote) as a filesystem @@ -284,8 +284,8 @@ class Gateway(object): if ar.td: self.ssl_context = ssl._create_unverified_context() elif ar.te: - self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) - self.ssl_context.load_verify_locations(ar.te) + self.ssl_context = ssl.create_default_context(cafile=ar.te) + self.ssl_context.check_hostname = ar.teh self.conns = {} @@ -1165,6 +1165,7 @@ NOTE: if server has --usernames enabled, then password is "username:password" ap2 = ap.add_argument_group("https/TLS") ap2.add_argument("-te", metavar="PEMFILE", help="certificate to expect/verify") + ap2.add_argument("-teh", action="store_true", help="require correct hostname in -te cert") ap2.add_argument("-td", action="store_true", help="disable certificate check") ap2 = ap.add_argument_group("cache/perf") diff --git a/bin/u2c.py b/bin/u2c.py index 16173cb4..cbd6595e 100755 --- a/bin/u2c.py +++ b/bin/u2c.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from __future__ import print_function, unicode_literals -S_VERSION = "2.16" -S_BUILD_DT = "2025-12-11" +S_VERSION = "2.17" +S_BUILD_DT = "2025-12-16" """ u2c.py: upload to copyparty @@ -165,8 +165,8 @@ class HCli(object): elif self.verify is True: self.ctx = None else: - self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS) - self.ctx.load_verify_locations(self.verify) + self.ctx = ssl.create_default_context(cafile=self.verify) + self.ctx.check_hostname = ar.teh self.base_hdrs = { "Accept": "*/*", @@ -1593,6 +1593,7 @@ NOTE: if server has --usernames enabled, then password is "username:password" ap = app.add_argument_group("tls") ap.add_argument("-te", metavar="PATH", help="path to ca.pem or cert.pem to expect/verify") + ap.add_argument("-teh", action="store_true", help="require correct hostname in -te cert") ap.add_argument("-td", action="store_true", help="disable certificate check") # fmt: on From 336842192c16b5fce652154bc816ff2ad2f884d7 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 20:38:37 +0000 Subject: [PATCH 62/67] add --ipar (reverseproxy-capable ipa); closes #1109 --- copyparty/__main__.py | 7 ++++--- copyparty/httpcli.py | 5 +++++ copyparty/httpconn.py | 1 + copyparty/httpsrv.py | 1 + tests/util.py | 3 ++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index b7716852..856a9757 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1279,7 +1279,8 @@ def add_network(ap): ap2.add_argument("--xf-host", metavar="NAME", type=u, default="x-forwarded-host", help="if reverse-proxied, which http header to read the correct Host value from; this header must contain the server's external domain name") ap2.add_argument("--xf-proto", metavar="NAME", type=u, default="x-forwarded-proto", help="if reverse-proxied, which http header to read the correct protocol value from; this header must contain either 'http' or 'https'") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") - ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") + ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]\n └─for performance and security, this only looks at the TCP/Network-level IP, and will NOT work behind a reverseproxy") + ap2.add_argument("--ipar", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated).\n └─this is reverseproxy-compatible; reads client-IP from 'X-Forwarded-For' if possible, with TCP/Network IP as fallback") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)") ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets") @@ -1422,7 +1423,7 @@ def add_ftp(ap): ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990") ap2.add_argument("--ftpv", action="store_true", help="verbose") ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4") - ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") + ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite") ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections") @@ -1448,7 +1449,7 @@ def add_tftp(ap): ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations") ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") - ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") + ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index c649c36b..6f448ade 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -472,6 +472,11 @@ class HttpCli(object): if self.is_banned(): return False + if self.conn.ipar_nm and not self.conn.ipar_nm.map(self.ip): + self.log("client rejected (--ipar)", 3) + self.terse_reply(b"", 500) + return False + if self.conn.aclose: nka = self.conn.aclose ip = ipnorm(self.ip) diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 11e00d1c..e36e5122 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -62,6 +62,7 @@ class HttpConn(object): self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm + self.ipar_nm: Optional[NetMap] = hsrv.ipar_nm self.xff_nm: Optional[NetMap] = hsrv.xff_nm self.xff_lan: NetMap = hsrv.xff_lan # type: ignore self.iphash: HMaccas = hsrv.broker.iphash diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 2d35f802..f6cdef40 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -201,6 +201,7 @@ class HttpSrv(object): self.ipr = None self.ipa_nm = build_netmap(self.args.ipa) + self.ipar_nm = build_netmap(self.args.ipar) self.xff_nm = build_netmap(self.args.xff_src) self.xff_lan = build_netmap("lan") diff --git a/tests/util.py b/tests/util.py index c6def6be..184caae4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,7 +164,7 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) - ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl rss_fmt_d rss_fmt_t spinner" @@ -355,6 +355,7 @@ class VHttpConn(object): self.ico = Ico(args) self.ipr = None self.ipa_nm = None + self.ipar_nm = None self.lf_url = None self.log_func = log self.log_src = "a" From 5e1d9a58d87d5920356986d5c1efce51b3534442 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 21:07:09 +0000 Subject: [PATCH 63/67] simplify idp-groups with spaces --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 3 +++ copyparty/svchub.py | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 856a9757..cef1a1f8 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1338,6 +1338,7 @@ def add_auth(ap): ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") + ap2.add_argument("--idp-chsub", metavar="TXT", type=u, default="", help="characters to replace in usernames/groupnames; a list of pairs of characters separated by | so for example | _| will replace spaces with _ to make configuration easier, or |%%_|^_|@_| will replace %%/^/@ with _") ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6f448ade..86bfca65 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -691,6 +691,9 @@ class HttpCli(object): if self.args.idp_h_grp else "" ) + if self.args.idp_chsub: + idp_usr = idp_usr.translate(self.args.idp_chsub_tr) + idp_grp = idp_grp.translate(self.args.idp_chsub_tr) if not trusted_xff: pip = self.conn.addr[0] diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 17f794c0..11b52a63 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -79,6 +79,7 @@ from .util import ( start_stackmon, termsize, ub64enc, + umktrans, ) if HAVE_SQLITE3: @@ -1131,8 +1132,19 @@ class SvcHub(object): except: raise Exception("invalid --idp-hm-usr [%s]" % (zs0,)) - al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True) - al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True) + zs1 = "" + zs2 = "" + zs = al.idp_chsub + while zs: + if zs[:1] != "|": + raise Exception("invalid --idp-chsub; expected another | but got " + zs) + zs1 += zs[1:2] + zs2 += zs[2:3] + zs = zs[3:] + al.idp_chsub_tr = umktrans(zs1, zs2) + + al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa or al.ipar, True) + al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa or al.ipar, True) mte = ODict.fromkeys(DEF_MTE.split(","), True) al.mte = odfusion(mte, al.mte) From 9c64788d438f970af58738390af35cc416ef4193 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 21:15:44 +0000 Subject: [PATCH 64/67] add x-forwarded-proto fallback (closes #1110); some reverseproxies do not include a compatible alternative to x-forwarded-proto by default, while also lacking the option to set custom headers add --xf-proto-fb to set a fixed protocol to assume --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index cef1a1f8..44e5f729 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1278,6 +1278,7 @@ def add_network(ap): ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xf-host", metavar="NAME", type=u, default="x-forwarded-host", help="if reverse-proxied, which http header to read the correct Host value from; this header must contain the server's external domain name") ap2.add_argument("--xf-proto", metavar="NAME", type=u, default="x-forwarded-proto", help="if reverse-proxied, which http header to read the correct protocol value from; this header must contain either 'http' or 'https'") + ap2.add_argument("--xf-proto-fb", metavar="T", type=u, default="", help="protocol to assume if the X-Forwarded-Proto header (\033[33m--xf-proto\033[0m) is not provided by the reverseproxy; either 'http' or 'https'") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]\n └─for performance and security, this only looks at the TCP/Network-level IP, and will NOT work behind a reverseproxy") ap2.add_argument("--ipar", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated).\n └─this is reverseproxy-compatible; reads client-IP from 'X-Forwarded-For' if possible, with TCP/Network IP as fallback") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 86bfca65..131a84b1 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -152,7 +152,8 @@ USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {} ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" -BADXFF2 = ". Some copyparty features are now disabled as a safety measure." +BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" +BADXFP = ', or change the copyparty global-option "xf-proto" to another header-name to read this value from. Alternatively, if your reverseproxy is not able to provide a header similar to "X-Forwarded-Proto", then you must tell copyparty which protocol to assume by setting global-option --xf-proto-fb to either http or https' H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -449,10 +450,13 @@ class HttpCli(object): try: self.is_https = len(self.headers[self.args.xf_proto]) == 5 except: - self.bad_xff = True - self.host = "example.com" - t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header, or change the copyparty global-option "xf-proto" to another header-name to read this value from' - self.log(t % (self.args.xf_proto,) + BADXFF2, 3) + if self.args.xf_proto_fb: + self.is_https = len(self.args.xf_proto_fb) == 5 + else: + self.bad_xff = True + self.host = "example.com" + t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header%s%s' + self.log(t % (self.args.xf_proto, BADXFP, BADXFF2), 3) # the semantics of trusted_xff and bad_xff are different; # trusted_xff is whether the connection came from a trusted reverseproxy, From c8f3b4ef055cb15f8fea16dcedfb510ebf607b6f Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 21:31:32 +0000 Subject: [PATCH 65/67] warning in controlpanel for rproxy misconfig --- copyparty/httpcli.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 131a84b1..f7261704 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -154,6 +154,7 @@ ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" BADXFP = ', or change the copyparty global-option "xf-proto" to another header-name to read this value from. Alternatively, if your reverseproxy is not able to provide a header similar to "X-Forwarded-Proto", then you must tell copyparty which protocol to assume by setting global-option --xf-proto-fb to either http or https' +BADXFFB = "NOTE: serverlog has a message regarding your reverse-proxy config" H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -423,7 +424,24 @@ class HttpCli(object): ] t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! The raw header value was [%s]. Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:' t = t % (self.args.xff_hdr, zso) - self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3) + t = "%s\n\n%s\n" % (t, "\n".join(zsl)) + + zs = self.headers.get(self.args.xf_proto) + t2 = "\nFurthermore, the following request-headers are also relevant, and you should check that the values below are sensible:\n\n request-header [%s] (configured with global-option --xf-proto) has the value [%s]; this should be the protocol that the webbrowser is using, so either 'http' or 'https'" + t += t2 % (self.args.xf_proto, zs or "NOT-PROVIDED") + if not zs: + t += ". Because the header is not provided by the reverse-proxy, you must either fix the reverseproxy config" + t += BADXFP + zs = self.headers.get(self.args.xf_host) + t2 = "\n\n request-header [%s] (configured with global-option --xf-host) has the value [%s]; this should be the website domain or external IP-address which the webbrowser is accessing" + t += t2 % (self.args.xf_host, zs or "NOT-PROVIDED") + if not zs: + zs = self.headers.get("host") + t2 = ". Because the header is not provided by the reverse-proxy, copyparty is using the standard [Host] header which has the value [%s]" + t += t2 % (zs or "NOT-PROVIDED") + if zs: + t += ". If that is the address that visitors are supposed to use to access your server -- or, in other words, it is not some internal address you wish to keep secret -- then the current choice of using the [Host] header is fine (usually the case)" + self.log(t + "\n\n\n", 3) pip = self.conn.addr[0] xffs = self.conn.xff_nm @@ -5437,6 +5455,7 @@ class HttpCli(object): no304=self.no304(), k304vis=self.args.k304 > 0, no304vis=self.args.no304 > 0, + msg=BADXFFB if hasattr(self, "bad_xff") else "", ver=S_VERSION if show_ver else "", chpw=self.args.chpw and self.uname != "*", ahttps="" if self.is_https else "https://" + self.host + self.req, From 8d46cf18237cae85853d39d8b6d99dbdb081c527 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 21:54:36 +0000 Subject: [PATCH 66/67] login-ui password max-length hint; closes #1029 --- copyparty/web/splash.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 5b3543ae..4a70d0f4 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -115,3 +115,9 @@ if (ebi('lf')) ebi('lm').innerHTML = un ? d.nou : d.nop; return false; }; + +if (ebi('lp')) + ebi('lp').oninput = function() { + ebi('lm').innerHTML = this.value.length <= 64 ? + '' : 'ERROR: Password too long (max=64)'; + }; From 0b6d2d2424930c6d923288a52809c7f815840d30 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 16 Dec 2025 22:38:51 +0000 Subject: [PATCH 67/67] safari: workaround another apple bug (closes #1111); seemingly as of iOS / macos 26.1, safari started requesting favicons -- specifically only favicons -- with the incorrect browser context (they probably forgot to initialize something) instead of the correct user-agent, it would send: * iOS: NetworkingExtension/8623.1.14.10.9 * macos: com.apple.WebKit.Networking/21623.1.14.11.9 further, it would NOT send any SameSite=Strict cookies, which the session-cookie is (for good reason) putting these two together, safari now looks like a webdav client, and copyparty sends the only appropriate response (http 401), resulting in a basic-authentication popup left with no good options, this is what we can do to mitigate: * add a new option --ua-nodav which is a regex of user-agents which are definitely not webdav clients, as macos-finder still flipflops between WebDAVLib/1.3 and WebDAVFS/3.0.0 like normal * use the "js=y" cookie as another flag that this is a webbrowser merry christmas --- copyparty/__main__.py | 3 ++- copyparty/httpcli.py | 7 ++++--- copyparty/svchub.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 44e5f729..76c991fa 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1439,7 +1439,8 @@ def add_webdav(ap): ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)") ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)") ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)") - ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of tricky user-agents which expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank") + ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of user-agents which ARE webdav-clients, and expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank") + ap2.add_argument("--ua-nodav", metavar="PTN", type=u, default=r"^(Mozilla/|NetworkingExtension/|com\.apple\.WebKit)", help="regex of user-agents which are NOT webdav-clients") def add_tftp(ap): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f7261704..6c2c646d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5466,7 +5466,7 @@ class HttpCli(object): def setck(self) -> bool: k, v = self.uparam["setck"].split("=", 1) t = 0 if v in ("", "x") else 86400 * 299 - ck = gencookie(k, v, self.args.R, self.args.cookie_lax, False, t) + ck = gencookie(k, v, self.args.R, True, False, t) self.out_headerlist.append(("Set-Cookie", ck)) if "cc" in self.ouparam: self.redirect("", "?h#cc") @@ -5478,7 +5478,7 @@ class HttpCli(object): for k in ALL_COOKIES: if k not in self.cookies: continue - cookie = gencookie(k, "x", self.args.R, self.args.cookie_lax, False) + cookie = gencookie(k, "x", self.args.R, True, False) self.out_headerlist.append(("Set-Cookie", cookie)) self.redirect("", "?h#cc") @@ -5512,8 +5512,9 @@ class HttpCli(object): rc == 403 and self.uname == "*" and "sec-fetch-site" not in self.headers + and self.cookies.get("js") != "y" and ( - not self.ua.startswith("Mozilla/") + not self.args.ua_nodav.search(self.ua) or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua)) ) ): diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 11b52a63..caa3ec9f 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1088,7 +1088,7 @@ class SvcHub(object): vsa = [x.lower() for x in vsa if x] setattr(al, k + "_set", set(vsa)) - zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip" + zs = "dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip" for k in zs.split(" "): vs = getattr(al, k) if not vs or vs == "no":