From 3b53a228b0e07912766d8dbf88b1dd01da57e2c5 Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Fri, 15 May 2026 21:51:12 +0200 Subject: [PATCH 01/32] invalidate get-only shares when creator no longer exists (#1482) Fixes #1480 --- copyparty/authsrv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 7dde17f4..38e1c9e2 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3113,7 +3113,7 @@ class AuthSrv(object): try: s_vfs, s_rem = vfs.get( - s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr + s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr, "g" in s_pr, ) except Exception as ex: t = "removing share [%s] by [%s] to [%s] due to %r" From 0bb80e92940834e1f9b42513b2231b1ddb1d23d6 Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Thu, 21 May 2026 00:27:25 +0200 Subject: [PATCH 02/32] auth: read (and admin) implies get (#1485) and hides redundant get-permission in webui; closes #1483 --- copyparty/authsrv.py | 3 ++- copyparty/httpcli.py | 1 + copyparty/web/browser.js | 12 +++++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 38e1c9e2..a1e18680 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1663,7 +1663,8 @@ class AuthSrv(object): for alias, mapping in [ ("h", "gh"), ("G", "gG"), - ("A", "rwmda.A"), + ("r", "g"), + ("A", "rgwmda.A"), ]: expanded = "" for ch in mapping: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index d8df0715..e4cd6a36 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6443,6 +6443,7 @@ class HttpCli(object): s_wr = "write" in req["perms"] s_get = "get" in req["perms"] s_dot = "dot" in req["perms"] + # will_read, will_write, will_move, will_del, will_get s_axs = [s_rd, s_wr, False, False, s_get] s_axsd = s_axs + [s_dot] diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 57685423..637edb69 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4145,6 +4145,8 @@ var fileman = (function () { if (this.textContent == 'write-only') for (var a = 0; a < pbtns.length; a++) clmod(pbtns[a], 'on', pbtns[a].textContent == 'write'); + if (this.textContent == 'get' && clgot(this, 'on') && has(perms, 'read')) + clmod(pbtns[0], 'on'); } clmod(pbtns[0], 'on', 1); @@ -8033,12 +8035,17 @@ function apply_perms(res) { a.style.display = ''; tt.att(QS('#ops')); + var v_perms = perms.slice(0); + var have_read = has(perms, 'read'); + if (have_read) + apop(v_perms, 'get'); + for (var a = 0; a < chk.length; a++) - if (has(perms, chk[a])) + if (has(v_perms, chk[a])) axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1)); axs = axs.join('-'); - if (perms.length == 1) { + if (v_perms.length == 1) { aclass = ' class="warn">'; axs += '-Only'; } @@ -8080,7 +8087,6 @@ function apply_perms(res) { document.body.setAttribute('perms', perms.join(' ')); var have_write = has(perms, "write"), - have_read = has(perms, "read"), de = document.documentElement, tds = QSA('#u2conf td'); From d7eb556cddad7958f21b51a36032c495170595c1 Mon Sep 17 00:00:00 2001 From: ilotoki0804 Date: Thu, 21 May 2026 07:30:37 +0900 Subject: [PATCH 03/32] docs: macOS low-ports + sfx building (#1458) Signed-off-by: ilotoki0804 --- README.md | 4 +--- docs/devnotes.md | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37131d1d..80cf852a 100644 --- a/README.md +++ b/README.md @@ -2321,9 +2321,7 @@ if you want to change the fonts, see [./docs/rice/](./docs/rice/) become a *real* webserver which people can access by just going to your IP or domain without specifying a port -**if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice - -**if you're on macos,** sorry, I don't know +**if you're on windows or macos,** then you just need to add the commandline argument `-p 80,443` and you're done! nice **if you're on Linux,** you have the following 4 options: diff --git a/docs/devnotes.md b/docs/devnotes.md index a0ce6bc1..afb86d9f 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -447,6 +447,12 @@ build the sfx using any of the following examples: ./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor ``` +on macos, you need to download several GNU utilities before building: + +```zsh +brew install gsed gnu-tar findutils coreutils +``` + ## build from release tarball From f432ef6d517494994c2db1d8d0a5c5cbf9f33cbb Mon Sep 17 00:00:00 2001 From: Princess Grace <10965841+Kansattica@users.noreply.github.com> Date: Wed, 20 May 2026 15:32:55 -0700 Subject: [PATCH 04/32] freebsd: fix deps in rc.d (#1479) delay startup until filesystems are in place Signed-off-by: Princess Grace <10965841+Kansattica@users.noreply.github.com> --- contrib/rc/copyparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/rc/copyparty b/contrib/rc/copyparty index 7fcc4e88..ba5b878d 100644 --- a/contrib/rc/copyparty +++ b/contrib/rc/copyparty @@ -1,7 +1,7 @@ #!/bin/sh # # PROVIDE: copyparty -# REQUIRE: networking +# REQUIRE: networking DAEMON FILESYSTEMS mountd # KEYWORD: . /etc/rc.subr From 83dc20f33e3f90b2faaae1effe7ec3941f74ea86 Mon Sep 17 00:00:00 2001 From: Lydia Vierkorn Date: Thu, 21 May 2026 00:34:48 +0200 Subject: [PATCH 05/32] spectrograms: option to use log frequency scale (#1487) --- copyparty/__main__.py | 1 + copyparty/th_srv.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 2f568b5c..1833aaec 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1769,6 +1769,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec") + ap2.add_argument("--th-spec-fl", action="store_true", help="generate spectrograms with logarithmic frequency scale instead of linear") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips # https://stackoverflow.com/a/47612661 diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 397f055d..f119c5e5 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -467,6 +467,11 @@ class ThumbSrv(object): 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_spec_fl" + for zs in zs.split(" "): + v = getattr(self.args, zs) + if v: + ret.append("%s(%s)\n" % (zs, v)) zs = "th_qv th_qvx thsize th_spec_p convt" for zs in zs.split(" "): ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) @@ -1056,11 +1061,13 @@ class ThumbSrv(object): # fmt: on self._run_ff(cmd, vn, "convt") + fscale = ":fscale=log" if self.args.th_spec_fl else "" + fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" if "3" in fmt: - fc += "1280x1024,crop=1420:1056:70:48[o]" + fc += "1280x1024%s,crop=1420:1056:70:48[o]" % fscale else: - fc += "640x512,crop=780:544:70:48[o]" + fc += "640x512%s,crop=780:544:70:48[o]" % fscale if self.args.th_ff_swr: fco = ":filter_size=128:cutoff=0.877" From 1f4246c6bb514d558f87a731d2eba99a5d0adff1 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 21 May 2026 00:27:41 +0000 Subject: [PATCH 06/32] support .mka; closes #1489 --- README.md | 2 +- copyparty/__main__.py | 2 +- copyparty/up2k.py | 2 +- copyparty/util.py | 2 +- copyparty/web/browser.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 80cf852a..5475eb54 100644 --- a/README.md +++ b/README.md @@ -1181,7 +1181,7 @@ open the `[🎺]` media-player-settings tab to configure it, * `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3 * `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3 * `[oth]` converts all other known formats into opus (if supported by browser) or mp3 - * `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk` + * `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk` * "transcode to": * `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs) * `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1 diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 1833aaec..51ceefab 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1779,7 +1779,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") - ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") + ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg") diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 6245085a..ff1ba97d 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -102,7 +102,7 @@ ICV_EXTS = set(zsg.split(",")) zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv" VCV_EXTS = set(zsg.split(",")) -zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub" +zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mka,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub" ACV_EXTS = set(zsg.split(",")) zsg = "nohash noidx xdev xvol" diff --git a/copyparty/util.py b/copyparty/util.py index 1f7b9cec..2d69d2fe 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -516,7 +516,7 @@ image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f -audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp +audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mka=x-matroska mpc=musepack aif=aiff au=basic qcp=qcelp video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr font ttc=collection diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 637edb69..fe162a8c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1701,7 +1701,7 @@ mpl.init_ac2(); var re_m3u = /\.(m3u8?)$/i; var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4[abr]|mp3|wav)$/i, re_au_vid = /\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i, - re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|b[cfr]stm|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i; + re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|b[cfr]stm|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i; // extract songs + add play column From b2401ff15a21a8ba389599f1d582ce41d36f9ceb Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 21 May 2026 23:31:57 +0000 Subject: [PATCH 07/32] partyfuse: prefer mfusepy (fuse.py fork); now supports both fuse2 and fuse3 fallback on fuse.py (fuse2-only) if mfusepy unavailable fuse3 is 20% faster on large files, fuse2 == fuse3 on small files motivated by nixos dropping fuse2 in NixOS/nixpkgs#522340 --- bin/README.md | 4 ++-- bin/partyfuse.py | 22 ++++++++++++++-------- copyparty/__init__.py | 2 +- copyparty/web/svcs.html | 2 +- docs/devnotes.md | 2 +- docs/lics.txt | 2 +- scripts/deps-docker/Dockerfile | 29 ++++++++++++++++++----------- scripts/deps-docker/Makefile | 1 + scripts/docker/Makefile | 2 +- scripts/sfx.ls | 2 +- 10 files changed, 41 insertions(+), 27 deletions(-) diff --git a/bin/README.md b/bin/README.md index 1fef13f4..b6a033b8 100644 --- a/bin/README.md +++ b/bin/README.md @@ -26,12 +26,12 @@ and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit ## to run this on windows: * install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/) * [x] add python 3.x to PATH (it asks during install) -* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder) +* `python -m pip install --user mfusepy` (or grab a copy of `mfusepy.py` from the `connect` page on your copyparty, and keep it in the same folder) * `python ./partyfuse.py n: http://192.168.1.69:3923/` 10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled: * `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}` -* `/mingw64/bin/python3 -m pip install --user fusepy` +* `/mingw64/bin/python3 -m pip install --user mfusepy` * `/mingw64/bin/python3 ./partyfuse.py [...]` you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE) diff --git a/bin/partyfuse.py b/bin/partyfuse.py index 7be355e1..4b6ab90e 100755 --- a/bin/partyfuse.py +++ b/bin/partyfuse.py @@ -21,7 +21,7 @@ usage: python partyfuse.py http://192.168.1.69:3923/ ./music dependencies: - python3 -m pip install --user fusepy # or grab it from the connect page + python3 -m pip install --user mfusepy # or grab it from the connect page + on Linux: sudo apk add fuse + on Macos: https://osxfuse.github.io/ + on Windows: https://github.com/billziss-gh/winfsp/releases/latest @@ -92,8 +92,13 @@ is_dbg = False try: - from fuse import FUSE, FuseOSError, Operations + from mfusepy import FUSE, FuseOSError, Operations except: + try: + from fuse import FUSE, FuseOSError, Operations + except: + FUSE = None + if WINDOWS: libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest" elif MACOS: @@ -102,12 +107,13 @@ except: libfuse = "apt install libfuse2\n modprobe fuse" m = """\033[33m - could not import fuse; these may help: - {} -m pip install --user fusepy + could not import mfusepy; these may help: + {} -m pip install --user mfusepy {} \033[0m""" - print(m.format(sys.executable, libfuse)) - raise + if not FUSE: + print(m.format(sys.executable, libfuse)) + raise def termsafe(txt): @@ -143,10 +149,10 @@ def fancy_log(fmt, *a): def register_wtf8(): - def wtf8_enc(text): + def wtf8_enc(text, errors=""): return str(text).encode("utf-8", "surrogateescape"), len(text) - def wtf8_dec(binary): + def wtf8_dec(binary, errors=""): return bytes(binary).decode("utf-8", "surrogateescape"), len(binary) def wtf8_search(encoding_name): diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 8bffed86..a698107c 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -77,7 +77,7 @@ web/deps/busy.mp3 web/deps/easymde.css web/deps/easymde.js web/deps/marked.js -web/deps/fuse.py +web/deps/mfusepy.py web/deps/mini-fa.css web/deps/mini-fa.woff web/deps/prism.css diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index bf1c1517..5daa5d22 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -233,7 +233,7 @@

partyfuse

partyfuse.py -- fast, read-only, - needs fuse.py in the same folder, + needs mfusepy.py in the same folder, needs winfsp doesn't need root

diff --git a/docs/devnotes.md b/docs/devnotes.md index afb86d9f..a1d90d8c 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -374,7 +374,7 @@ some third-party code has been vendored into the git repo; some for convenience, * `dnslib` (MIT) may be deleted and replaced with a systemwide install of the original [dnslib](https://github.com/paulc/dnslib/), HOWEVER: * will cause problems for mDNS in some network environments; 6c1cf68bca7376c6291c3cfe710ebd5bd5ed3e6c + 94d1924fa97e5faaf1ebfd85cae73faebcb89fa1 -* inside the folder `/copyparty/web/deps` (only in distributed archives/builds) is [fuse.py](https://github.com/fusepy/fusepy/blob/master/fuse.py), to make it downloadable from the connect-page on the web-ui +* inside the folder `/copyparty/web/deps` (only in distributed archives/builds) is [mfusepy.py](https://github.com/mxmlnkn/mfusepy/blob/master/mfusepy.py) (sizegolfed, no important changes), to make it downloadable from the connect-page on the web-ui * inside the folder `/copyparty/web` (only in distributed archives/builds) is a collection of javascript libraries (produced by [deps-docker](https://github.com/9001/copyparty/tree/hovudstraum/scripts/deps-docker)) which are used clientside by the web-UI: * [marked.js](https://github.com/markedjs/marked/releases) (MIT) powers the markdown editor, and has been [patched](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/marked-ln.patch) to include the line-numbers of each input line, to enable scroll-sync between the editor and the preview-pane. This patch is [not strictly necessary anymore](https://github.com/markedjs/marked/issues/2134) but I haven't gotten around to making the change yet diff --git a/docs/lics.txt b/docs/lics.txt index cf1edd2f..7380eada 100644 --- a/docs/lics.txt +++ b/docs/lics.txt @@ -36,7 +36,7 @@ https://github.com/ahupp/python-magic/ C: 2001-2014 Adam Hupp L: MIT -https://github.com/fusepy/fusepy +https://github.com/mxmlnkn/mfusepy C: 2012 Giorgos Verigakis L: ISC diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index 9f236e88..6f8663e8 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -24,8 +24,9 @@ RUN mkdir -p /z/dist/no-pk \ && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \ && apk add \ bash brotli cmake make g++ git gzip lame npm patch pigz \ - python3 python3-dev py3-brotli sox tar unzip wget \ + python3 python3-dev py3-pip py3-brotli sox tar unzip wget \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ + && pip install strip_hints \ && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \ && wget https://github.com/Ionaru/easy-markdown-editor/archive/$ver_mde.tar.gz -O mde.tgz \ && wget https://github.com/codemirror/codemirror5/archive/$ver_codemirror.tar.gz -O codemirror.tgz \ @@ -34,7 +35,7 @@ RUN mkdir -p /z/dist/no-pk \ && wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \ && wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \ && wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \ - && wget https://files.pythonhosted.org/packages/04/0b/4506cb2e831cea4b0214d3625430e921faaa05a7fb520458c75a2dbd2152/fusepy-3.0.1.tar.gz -O fusepy.tgz \ + && wget https://files.pythonhosted.org/packages/91/47/746287c8962274f73ee25edb3840d80899464bfffbe2c435424c2d60a071/mfusepy-3.1.1.tar.gz -O mfusepy.tgz \ && (mkdir hash-wasm \ && cd hash-wasm \ && unzip ../hash-wasm.zip) \ @@ -51,7 +52,7 @@ RUN mkdir -p /z/dist/no-pk \ && npm i gulp-cli -g ) \ && tar --no-same-owner -xf dompurify.tgz \ && tar --no-same-owner -xf prism.tgz \ - && tar --no-same-owner -xf fusepy.tgz \ + && tar --no-same-owner -xf mfusepy.tgz \ && unzip fontawesome.zip \ && tar --no-same-owner -xf zopfli.tgz @@ -148,16 +149,22 @@ RUN cd /z/dist \ && rmdir no-pk -# build fusepy -COPY uncomment.py /z -RUN mv /z/fusepy-3.0.1/fuse.py /z/dist/f1 \ +# build mfusepy -- just sizegolfing for the sfx, mfusepy.py works fine as-is +COPY uncomment.py unhint.py /z +RUN mv /z/mfusepy-3.1.1/mfusepy.py /z/dist/ \ && cd /z/dist \ + && python3 /z/unhint.py \ + && mv mfusepy.py f1 \ && python3 /z/uncomment.py f1 \ - && sed -ri '/self.__critical_exception = e/d' f1 \ - && awk '/^log =/{s=0} !s; /^from traceback im/{s=1;print"from functools import partial";print"basestring = str"}' f2 \ - && awk '/LoggingMixIn:/{exit} --s<0;/self.use_ns = getattr/{s=7}' f1 \ - && awk "/if _machine =/{s=0} /'(mips|ppc|ppc64)'/{s=1} !s" f2 \ - && rm f1 && mv f2 fuse.py + && sed -ri '/self.__critical_exception/d; /^from (typing|collections.abc) import/d' f1 \ + && sed -ri '/^(FieldsEntry|BitFieldsEntry|ReadDirResult) =/d' f1 \ + && awk "/if TYPE_CHECKING:/{s=1;sub(/TYPE_CHECKING/,"1");print}!s;/else:/{s=0}" f2 \ + && awk '/^def _nullable_dummy_function/{print"class Operations:\n pass";exit};1' f1 \ + && awk '/^# Note that/{s=3} --s<0; /self.use_ns = getattr/{s=16}' f2 \ + && awk "/if _machine =/{s=0} /_machine == '(mips|ppc|ppc64)'/{s=1} !s" f1 \ + && awk '/else:/{s=0} /elif _system == .NetBSD/{s=1} !s' f2 \ + && awk '/^if _system == .NetBSD_False/{s=1;print"if 0:\n pass"} /^elif/{s=0} !s' f1 \ + && rm f2 && mv f1 mfusepy.py # git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch diff --git a/scripts/deps-docker/Makefile b/scripts/deps-docker/Makefile index dcd1f03f..63043a2e 100644 --- a/scripts/deps-docker/Makefile +++ b/scripts/deps-docker/Makefile @@ -5,6 +5,7 @@ vend := $(self)/../../copyparty/web/deps all: cp -pv ../uncomment.py . + cp -pv ../strip_hints/a.py unhint.py docker build -t build-copyparty-deps . diff --git a/scripts/docker/Makefile b/scripts/docker/Makefile index 26c7d8e3..e36e3d45 100644 --- a/scripts/docker/Makefile +++ b/scripts/docker/Makefile @@ -41,7 +41,7 @@ hclean: purge: -docker kill `docker ps -q` -docker rm `docker ps -qa` - -docker rmi `docker images -qa` + -docker rmi `docker images -qa` -f sh: @printf "\n\033[1;31mopening a shell in the most recently created docker image\033[0m\n" diff --git a/scripts/sfx.ls b/scripts/sfx.ls index 66ad6133..eb24b478 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -80,7 +80,7 @@ copyparty/web/deps/__init__.py, copyparty/web/deps/busy.mp3, copyparty/web/deps/easymde.css, copyparty/web/deps/easymde.js, -copyparty/web/deps/fuse.py, +copyparty/web/deps/mfusepy.py, copyparty/web/deps/marked.js, copyparty/web/deps/mini-fa.css, copyparty/web/deps/mini-fa.woff, From 8c201b844ee71b4ab0271aa0be90986e8de98ea6 Mon Sep 17 00:00:00 2001 From: Mobin Date: Fri, 22 May 2026 20:56:47 +0330 Subject: [PATCH 08/32] s6-notify: support fd-based s6 notification protocol (#1466) Dinit and s6 supervision suite are known to support this protocol. See https://skarnet.org/software/s6/notifywhenup.html and https://davmac.org/projects/dinit/man-pages-html/dinit-service.5.html#ready Signed-off-by: Mobin Aydinfar --- copyparty/svchub.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index fc0a9db9..1e74321d 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -987,6 +987,10 @@ class SvcHub(object): Daemon(self.sd_notify, "sd-notify") + zb = os.environ.get("S6_NOTIFY_FD") + if zb: + Daemon(self.s6_notify, "s6-notify", (zb,)) + def _feature_test(self) -> None: fok = [] fng = [] @@ -1897,6 +1901,17 @@ class SvcHub(object): except: self.log("sd_notify", min_ex()) + def s6_notify(self, zb: bytes) -> None: + try: + fd = int(zb) + if fd < 3: + raise Exception("value < 3") + os.write(fd, b"\n") + os.close(fd) + except: + t = "S6_NOTIFY_FD=%s:\n%s" + self.log("s6-notify", t % (zb, min_ex()), 1) + def log_stacks(self) -> None: td = time.time() - self.tstack if td < 300: From 9b0268970cf843d3b64f9bbdb8558f091ccfb89f Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 22 May 2026 17:42:04 +0000 Subject: [PATCH 09/32] sd_notify only when set --- copyparty/svchub.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 1e74321d..2a4737fc 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -985,7 +985,9 @@ class SvcHub(object): def after_httpsrv_up(self) -> None: self.up2k.init_vols() - Daemon(self.sd_notify, "sd-notify") + zb = os.environ.get("NOTIFY_SOCKET") + if zb: + Daemon(self.sd_notify, "sd-notify", (zb,)) zb = os.environ.get("S6_NOTIFY_FD") if zb: @@ -1882,12 +1884,8 @@ class SvcHub(object): self.log("svchub", "cannot efficiently use multiple CPU cores") return False - def sd_notify(self) -> None: + def sd_notify(self, zb: bytes) -> None: try: - zb = os.environ.get("NOTIFY_SOCKET") - if not zb: - return - addr = unicode(zb) if addr.startswith("@"): addr = "\0" + addr[1:] @@ -1899,7 +1897,8 @@ class SvcHub(object): sck.connect(addr) sck.sendall(b"READY=1") except: - self.log("sd_notify", min_ex()) + t = "NOTIFY_SOCKET=%s:\n%s" + self.log("sd-notify", t % (zb, min_ex()), 1) def s6_notify(self, zb: bytes) -> None: try: From 30b23c6ae816d7e8142b395130934c5e02317331 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 22 May 2026 17:42:41 +0000 Subject: [PATCH 10/32] black --- copyparty/authsrv.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index a1e18680..b261a1f4 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3114,7 +3114,13 @@ class AuthSrv(object): try: s_vfs, s_rem = vfs.get( - s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr, "g" in s_pr, + s_vp, + s_un, + "r" in s_pr, + "w" in s_pr, + "m" in s_pr, + "d" in s_pr, + "g" in s_pr, ) except Exception as ex: t = "removing share [%s] by [%s] to [%s] due to %r" From ca406472f4b45f68842bc31716deb52b66698637 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 22 May 2026 21:35:59 +0000 Subject: [PATCH 11/32] readme: S6_NOTIFY_FD --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 5475eb54..b48dfa60 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,11 @@ you may also want these, especially on servers: * [nixos module](#nixos-module) to run copyparty on NixOS hosts * [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https) +because the following environment variables are commonly used in service-scripts, they are understood by copyparty: + +* `NOTIFY_SOCKET` as provided by systemd with service type=notify (see systemd/copyparty.service above) +* `S6_NOTIFY_FD` for s6/dinit [`ready-notification = pipevar:S6_NOTIFY_FD`](https://skarnet.org/software/s6/notifywhenup.html) + and remember to open the ports you want; here's a complete example including every feature copyparty has to offer: ``` firewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp # --zone=libvirt From 7d81b9e8372fbecda8572ae7d47090330c25bb7c Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Sat, 23 May 2026 17:15:00 +0300 Subject: [PATCH 12/32] fix dsel error with bbox active (#1494) stop error when dragging outside window with dsel active; fixes 1491 --- copyparty/web/browser.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index fe162a8c..6622c5c1 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -10036,7 +10036,7 @@ function reload_browser() { if (e.target.closest('#widget,#ops,.opview,.doc')) return; if (e.target.closest('#gfiles')) - ebi('gfiles').style.userSelect = "none" + ebi('gfiles').style.userSelect = "none"; var pos = getpp(e); startx = pos.x; @@ -10077,7 +10077,9 @@ function reload_browser() { return; } if (!dragging && dist > mvthresh && !window.getSelection().toString()) { - if (fwrap = e.target.closest('#wrap')) + if (e.target instanceof Element) + fwrap = e.target.closest('#wrap'); + if (fwrap) fwrap.style.userSelect = 'none'; else return; start_drag(); @@ -10121,7 +10123,8 @@ function reload_browser() { window.addEventListener('dragstart', function(e) { if (treectl.dsel && (is_selma || dragging)) { - e.preventDefault(); + if (!QS('body.bbox-open')) + ev(e); } }); } From 2c778e0828fac843f51adbfc510ab207df0de179 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 23 May 2026 17:54:10 +0000 Subject: [PATCH 13/32] add font orbitron for ui-v1.5 --- scripts/deps-docker/Dockerfile | 2 ++ scripts/make-sfx.sh | 1 + 2 files changed, 3 insertions(+) diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index 6f8663e8..a79d9426 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -21,6 +21,8 @@ ENV ver_hashwasm=4.12.0 \ # download; # the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap RUN mkdir -p /z/dist/no-pk \ + && wget https://ocv.me/dev/fonts/orbitron.woff2 -O /z/dist/no-pk/orbitron.woff2 \ + && sha512sum /z/dist/no-pk/orbitron.woff2 | tee /dev/stderr | grep -q b36a69a1b483ca735a3e7d026cdc5e993b8359bbf69b05deadc369fa759f88fe6087bbbbc0be51e62daf034c090df7ee096e5b07884c494aebf56db0e2bc53f7 \ && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \ && apk add \ bash brotli cmake make g++ git gzip lame npm patch pigz \ diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index af4060b7..f6f86e09 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -511,6 +511,7 @@ unhelpg() { [ $no_hl ] && rm -rf copyparty/web/deps/prism* +rm -f copyparty/web/deps/orbitron.woff2 # todo:uiv15 [ $no_fnt ] && { rm -f copyparty/web/deps/scp.woff2 f=copyparty/web/ui.css From 348b4bb5c762f851d866e200ee0c4c44ea477f3a Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 23 May 2026 20:55:33 +0000 Subject: [PATCH 14/32] drop rawpy, use libraw/dcraw_emu directly; rawpy is still supported but will not be bundled by default due to security concerns dcraw_emu reads more formats than rawpy + gives better quality (we told rawpy to use embedded thumbs), so also much slower dcraw_emu must be combined with libvips or pillow (equivalent) other alternatives considered: libvips + a full imagemagick does a different subset of formats, less than dcraw_emu, yet is 3x slower and eats ram magick wins wrt formats but is even slower (4x of dcraw_emu) --- README.md | 8 +++-- copyparty/__main__.py | 4 +-- copyparty/svchub.py | 8 +++-- copyparty/th_srv.py | 51 ++++++++++++++++++++++++---- docs/th-raw.txt | 64 ++++++++++++++++++++++++++++++++++++ scripts/docker/Dockerfile.dj | 8 ++--- scripts/docker/Dockerfile.iv | 12 ++----- 7 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 docs/th-raw.txt diff --git a/README.md b/README.md index b48dfa60..4f887359 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ also see [comparison to similar software](./docs/versus.md) * β˜‘ realtime streaming of growing files (logfiles and such) * β˜‘ [thumbnails](#thumbnails) * β˜‘ ...of images using Pillow, pyvips, or FFmpeg - * β˜‘ ...of RAW images using rawpy + * β˜‘ ...of RAW images using libraw-dcraw_emu or rawpy * β˜‘ ...of videos using FFmpeg * β˜‘ ...of audio (spectrograms) using FFmpeg * β˜‘ cache eviction (max-age; maybe max-size eventually) @@ -3206,7 +3206,7 @@ enable [thumbnails](#thumbnails) of... * **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif` * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+ * **JPEG XL pictures:** `pyvips` or `ffmpeg` -* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats) +* **RAW photos:** either `libraw dcraw_emu` or `rawpy`, plus either `pyvips` or `Pillow` enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq` @@ -3232,6 +3232,7 @@ set any of the following environment variables to disable its associated optiona | -------------------- | ------------ | | `PRTY_NO_ARGON2` | disable argon2-cffi password hashing | | `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) | +| `PRTY_NO_DCRAW` | disable all [libraw](https://www.libraw.org/homepage)-based thumbnail support for RAW images | | `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips | | `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen | | `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection | @@ -3246,7 +3247,8 @@ set any of the following environment variables to disable its associated optiona | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | | `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) | -| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images | +| `PRTY_NO_RAW` | same as `PRTY_NO_DCRAW` plus `PRTY_NO_RAWPY` | +| `PRTY_NO_RAWPY` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images | | `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg | example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 51ceefab..b2f72c61 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1775,8 +1775,8 @@ def add_thumbnail(ap): # https://stackoverflow.com/a/47612661 # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") - ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,mdc,mef,mrw,nef,nii,pfm,pgm,png,ppm,raf,raw,sr2,srf,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips") - ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy") + ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,arw,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,kdc,mdc,mef,mrw,nef,nii,nrw,orf,pfm,pgm,png,ppm,raf,raw,rw2,sr2,srf,srw,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips") + ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,rw2,sr2,srf,srw,x3f", help="image formats to decode using rawpy (if available) or libraw's dcraw_emu") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 2a4737fc..0f4325d0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -41,10 +41,11 @@ from .th_srv import ( H_PIL_AVIF, H_PIL_HEIF, H_PIL_WEBP, + HAVE_DCRAW, HAVE_FFMPEG, HAVE_FFPROBE, HAVE_PIL, - HAVE_RAW, + HAVE_RAWPY, HAVE_VIPS, ThumbSrv, ) @@ -398,7 +399,7 @@ class SvcHub(object): decs.pop("vips", None) if not HAVE_PIL: decs.pop("pil", None) - if not HAVE_RAW: + if not HAVE_RAWPY and not HAVE_DCRAW: decs.pop("raw", None) if not HAVE_FFMPEG or not HAVE_FFPROBE: decs.pop("ff", None) @@ -1009,7 +1010,8 @@ class SvcHub(object): (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (H_PIL_HEIF, "pillow-heif", "read .heif pics with pillow (rarely useful)"), (H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"), - (HAVE_RAW, "rawpy", "read RAW images"), + (HAVE_RAWPY, "rawpy", "read RAW images"), + (HAVE_DCRAW, "libraw", "read RAW images"), ] if ANYWIN: to_check += [ diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index f119c5e5..e63ce14e 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -18,7 +18,7 @@ from queue import Queue 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 +from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe, have_ff from .util import BytesIO # type: ignore from .util import ( FFMPEG_URL, @@ -201,16 +201,22 @@ except Exception as e: logging.warning("libvips found, but failed to load: " + str(e)) +PRTY_NO_RAW = os.environ.get("PRTY_NO_RAW") +PRTY_NO_RAWPY = PRTY_NO_RAW or os.environ.get("PRTY_NO_RAWPY") +PRTY_NO_DCRAW = PRTY_NO_RAW or os.environ.get("PRTY_NO_DCRAW") try: - if os.environ.get("PRTY_NO_RAW"): + if PRTY_NO_RAWPY: raise Exception() - HAVE_RAW = True + HAVE_RAWPY = True import rawpy logging.getLogger("rawpy").setLevel(logging.WARNING) except: - HAVE_RAW = False + HAVE_RAWPY = False + + +HAVE_DCRAW = not PRTY_NO_DCRAW and have_ff("dcraw_emu") th_dir_cache = {} @@ -307,6 +313,8 @@ class ThumbSrv(object): if ANYWIN and self.args.no_acode: self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) + self.conv_raw = self._conv_rawpy if HAVE_RAWPY else self._conv_dcraw + if self.args.th_clean: Daemon(self.cleaner, "thumb.cln") @@ -758,8 +766,39 @@ class ThumbSrv(object): self.conv_image_vips(_loader, tpath, fmt, vn) - def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - self.wait4ram(0.2, tpath) + def _conv_dcraw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: + self.wait4ram(0.6, tpath) + # fmt: off + cmd = [ + b"dcraw_emu", + b"-h", # halfsize + b"-o", b"1", # srgb + b"-s", b"0", # first frame + b"-Z", b"-", # to stdout + fsenc(abspath), + ] + # fmt: on + p = sp.Popen(cmd, stdout=sp.PIPE) + try: + if HAVE_PIL: + self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn) + elif HAVE_VIPS: + ppm, _ = p.communicate(timeout=vn.flags["convt"]) + + def _loader(w: int, kw: dict) -> Any: + return pyvips.Image.thumbnail_buffer(ppm, w, **kw) + + self.conv_image_vips(_loader, tpath, fmt, vn) + else: + raise Exception( + "either pil or vips is needed to process embedded bitmap thumbnails in raw files" + ) + finally: + if p and p.poll() is None: + p.kill(9) + + def _conv_rawpy(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: + self.wait4ram(0.6, tpath) with rawpy.imread(abspath) as raw: thumb = raw.extract_thumb() if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"): diff --git a/docs/th-raw.txt b/docs/th-raw.txt new file mode 100644 index 00000000..5a25dee5 --- /dev/null +++ b/docs/th-raw.txt @@ -0,0 +1,64 @@ +FS=/home/ed/Pictures/rawsamples-ch # https://rawsamples.ch/index.php/en/ (the 7z) + +find $FS -type f | sed -r 's/(.*)\.(.*)/\2 \1.\2/' | sort | tr '[:upper:]' '[:lower:]' | uniq -cw16 | sort -n | awk '{printf"%s ",$2}' +FMTS="dcr erf mdc mef ppm sr2 srf mos pdf 3fr tiff nrw kdc tif srw x3f mrw pef dng raw raf arw crw orf nef cr2 rw2 jpg" + +for w in $FMTS ; do grep -E "th-r-.*\b$w\b" ~/dev/copyparty/copyparty/__main__.py || echo "$w"; done +missing rw2; +FMTS_CPP=3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f,rw2 + +rm -rf $FS/.hist +time podman run --rm -it -v $FS:/w copyparty/iv -v /w::r --exit=thgen --th-pregen=j +find $FS/.hist/th/ -iname '*.jpg' | wc -l + +371 0m5.200s 3s +458 0m5.512s 3s with --th-r-raw=$FMTS_CPP +443 0m3.967s 2s with --th-r-raw=$FMTS_CPP --th-dec=raw + +t0=$(date +%s); for f in $FMTS ; do rm -rf $FS/.hist +podman run --rm -it -v $FS:/w copyparty/iv -v /w::r --exit=thgen --th-pregen=j --th-dec=raw --th-r-raw=$f -q >/dev/null 2>&1 +printf '%s ' $(find $FS/.hist/th/ -size +0 -iname '*.jpg' | wc -l) +done;t=$(date +%s);echo $((t-t0)) + + 95f 55s --th-dec=ff +397f 51s --th-dec=raw (rawpy) +153f 51s --th-dec=vips (no-magick) + +# swithc to persistent for messingaround +podman run --rm -it -v $FS:/w --entrypoint /bin/ash copyparty/iv +apk update +apk upgrade -lai + +t0=$(date +%s); for f in $FMTS ; do rm -rf $FS/.hist +podman exec -it d171470581ab python3 -m copyparty -v /w::r --exit=thgen --th-pregen=j --th-dec=vips --th-r-vips=$f -q >/dev/null 2>&1 +printf '%s ' $(find $FS/.hist/th/ -size +0 -iname '*.jpg' | wc -l) +done;t=$(date +%s);echo $((t-t0)) +# equivalent results + +apk add imagemagick; t0=$(date +%s); rm -rf /w/.hist/ ; for f in $FMTS ; do rm -f /*.jpg; n=0; find /w -type f -iname "*.$f" | while IFS= read -r x; do magick "$x" -scale 320x /$n.jpg >/dev/null 2>&1 ; [ -s /$n.jpg ] || rm -f /$n.jpg; n=$((n+1)); done; echo -n "$(ls -1 / | grep -F .jpg | wc -l) "; done; t=$(date +%s); echo $((t-t0)) + +apk add libraw-tools; t0=$(date +%s); rm -rf /w/.hist/ ; for f in $FMTS ; do rm -f /*.jpg; n=0; find /w -type f -iname "*.$f" | while IFS= read -r x; do [ $(dcraw_emu -h -o 1 -s 0 -Z - "$x" 2>/dev/null | wc -c) -gt 1024 ] && touch /$n.jpg; n=$((n+1)); done; echo -n "$(ls -1 / | grep -F .jpg | wc -l) "; done; t=$(date +%s); echo $((t-t0)) + +dcr erf mdc mef ppm sr2 srf mos pdf 3fr tiff nrw kdc tif srw x3f mrw pef dng raw raf arw crw orf nef cr2 rw2 jpg +d e m m p s s m p 3 t n k t s x m p d r r a c o n c r j +c r d e p r r o d f i r d i r 3 r e n a a r r r e r w p +r f c f m 2 f s f r f w c f w f w f g w f w w f f 2 2 g +----------------------------------------------------------------- +0 0 0 0 1 1 0 0 0 0 3 0 2 6 0 0 0 6 17 0 0 0 0 0 54 0 0 5 = 95, 55s --th-dec=ff +1 1 0 1 0 1 1 2 0 3 0 4 5 6 7 0 8 17 16 2 30 31 17 45 56 56 87 0 =397, 51s --th-dec=raw ## rawpy +1 1 1 1 0 1 1 2 0 3 0 4 5 6 6 0 9 17 18 24 30 31 34 45 56 57 87 0 =440, 87s dcraw_emu +1 1 1 1 1 1 1 2 0 3 6 4 5 6 6 0 9 17 18 24 30 31 34 45 56 57 87 5 =452, 226s magick-cmd +0 1 0 1 1 0 0 0 2 3 3 0 3 6 0 0 0 0 17 0 0 0 0 0 56 55 0 5 =153, 51s --th-dec=vips ## stock +0 1 0 1 1 1 1 0 2 3 3 4 5 6 6 0 8 0 18 11 30 30 18 39 56 55 87 5 =391, 151s vips + apk add imagemagick imagemagick-raw +0 1 0 1 1 0 0 0 2 3 3 0 3 6 0 0 8 0 17 11 30 0 18 39 56 55 87 5 =346, 128s vips + apk del imagemagick (just imagemagick-raw) + +xsel -o | tr ' ' '\n' | awk '!$0{next} {t+=$1} END{print t}' + +apk add time +rm -rf /w/.hist; time python3 -m copyparty -v /w::r --exit=thgen --th-pregen=j --th-dec=raw --th-r-raw=$FMTS_CPP ; find /w/.hist/ -iname '*.jpg' -size +0 | wc -l + +391f, 0:03.77elapsed 227264maxresident # rawpy+vips with-embedded-thumbs +391f, 0:04.79elapsed 467036maxresident # rawpy+pillow with-embedded-thumbs +434f, 0:29.67elapsed 307724maxresident # dcraw+vips +434f, 0:29.34elapsed 327980maxresident # dcraw+pillow +374f, 1:49.70elapsed 4574768maxresident # vips+imagemagick lol lmao diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index ec82be50..1d1233f0 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -15,22 +15,20 @@ RUN apk add -U !pyc ${ADD_PKG} \ tzdata wget mimalloc2 mimalloc2-insecure \ py3-jinja2 py3-argon2-cffi py3-pyzmq \ py3-openssl py3-paramiko py3-pillow \ - py3-pip py3-cffi \ + py3-pip \ py3-magic \ vips-jxl vips-poppler vips-magick \ py3-numpy fftw libsndfile \ vamp-sdk vamp-sdk-libs keyfinder-cli \ - libraw py3-numpy \ + libraw-tools \ && apk add -t .bd \ bash wget gcc g++ make cmake patchelf \ ffmpeg ffmpeg-dev \ python3-dev fftw-dev libsndfile-dev \ - py3-wheel py3-numpy-dev libffi-dev \ + py3-wheel py3-numpy-dev \ vamp-sdk-dev \ - libraw-dev py3-numpy-dev cython \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && python3 -m pip install pyvips \ - && python3 -m pip install --no-binary rawpy rawpy \ && bash install-deps.sh \ && apk del py3-pip .bd \ && chmod 777 /root \ diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index dff98cd0..b85bf846 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -12,18 +12,12 @@ RUN apk add -U !pyc ${ADD_PKG} \ tzdata wget mimalloc2 mimalloc2-insecure \ py3-jinja2 py3-argon2-cffi py3-pyzmq \ py3-openssl py3-paramiko py3-pillow \ - py3-pip py3-cffi \ + py3-pip \ py3-magic \ vips-jxl vips-poppler vips-magick \ - libraw py3-numpy \ - && apk add -t .bd \ - bash wget gcc g++ make cmake patchelf \ - python3-dev py3-wheel libffi-dev \ - libraw-dev py3-numpy-dev cython \ + libraw-tools \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ - && python3 -m pip install pyvips \ - && python3 -m pip install --no-binary rawpy rawpy \ - && apk del py3-pip .bd + && python3 -m pip install pyvips COPY i innvikler.sh ./ RUN ash innvikler.sh iv From f23ec5d9f8e5de768254d5b95a79bae0c893ec20 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 23 May 2026 20:56:44 +0000 Subject: [PATCH 15/32] tests: 0bb80e92 --- tests/test_idp.py | 5 ++++- tests/test_vfs.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_idp.py b/tests/test_idp.py index 3378d3b1..b2ec0580 100644 --- a/tests/test_idp.py +++ b/tests/test_idp.py @@ -38,7 +38,10 @@ class TestVFS(unittest.TestCase): unpacked.append(list(sorted(getattr(axs, k)))) pad = len(unpacked) - len(expected) - self.assertEqual(unpacked, expected + [[]] * pad) + want = expected + [[]] * pad + if want[0] and not want[4]: + want[4] = want[0] + self.assertEqual(unpacked, want) def assertAxsAt(self, au, vp, expected): vn = self.nav(au, vp) diff --git a/tests/test_vfs.py b/tests/test_vfs.py index bd7432b7..0278f8f1 100644 --- a/tests/test_vfs.py +++ b/tests/test_vfs.py @@ -188,8 +188,8 @@ class TestVFS(unittest.TestCase): self.assertAxs(n.axs.uread, ["*", "k"]) self.assertAxs(n.axs.uwrite, []) perm_na = (False, False, False, False, False, False, False, False, False) - perm_rw = (True, True, False, False, False, False, False, False, False) - perm_ro = (True, False, False, False, False, False, False, False, False) + perm_rw = (True, True, False, False, True, False, False, False, False) + perm_ro = (True, False, False, False, True, False, False, False, False) self.assertEqual(vfs.can_access("/", "*"), perm_na) self.assertEqual(vfs.can_access("/", "k"), perm_rw) self.assertEqual(vfs.can_access("/a", "*"), perm_ro) From f4f97b6cc3044700bb0b5cb3dc32baaf634c3095 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 00:37:52 +0000 Subject: [PATCH 16/32] make signal-handler less shit; previously: threading.Condition to wakeup the actual handler; exciting chance of heisenbugs / deadlocks (theoretically) almost went with os.pipe on unix and socketpairs on windows, but turns out SimpleQueue is perfect and safe for this purpose SimpleQueue is 3.7+ so use a regular queue on <3.7 (same problems as original approach) also need dedicated thread for popping the queue on <3.7 to avoid deadlock on most platforms (--sig-thr) new features: --logrot-sig sets a signal for immediate log-rotate --stack-sig sets a signal to dump stack to log/stdout --reload-sig sets a signal to initiates config-reload (was hardcoded to USR1 previously) --- copyparty/__main__.py | 7 ++++ copyparty/svchub.py | 98 +++++++++++++++++++++++++------------------ copyparty/util.py | 10 +++++ 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index b2f72c61..8eb39cd2 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1234,6 +1234,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best") + ap2.add_argument("--reload-sig", metavar="S", type=u, default=("" if ANYWIN else "USR1"), help="reload server config when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR1\033[0m], [\033[32mUSR1\033[0m], [\033[32m10\033[0m]") ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)") ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks") ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable") @@ -1706,6 +1707,7 @@ def add_logging(ap): ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m") + ap2.add_argument("--logrot-sig", metavar="S", type=u, default="", help="immediately logrotate when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGHUP\033[0m], [\033[32mHUP\033[0m], [\033[32m1\033[0m]") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") @@ -1980,10 +1982,15 @@ def add_debug(ap): ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead") ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") + if ANYWIN or sys.version_info < (3, 7): + ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS) + else: + ap2.add_argument("--sig-thr", action="store_true", help="start separate thread for OS-signals (try this if CTRL-C is busted)") ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind") ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks") ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") + ap2.add_argument("--stack-sig", metavar="S", type=u, default="", help="show stacktrace when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR2\033[0m], [\033[32mUSR2\033[0m], [\033[32m12\033[0m]") ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m") ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files") ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 0f4325d0..28f5cc0c 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -82,6 +82,7 @@ from .util import ( odfusion, pybin, read_utf8, + signame2int, start_log_thrs, start_stackmon, termsize, @@ -107,6 +108,12 @@ if PY2: else: from urllib.request import Request, urlopen +try: + from queue import SimpleQueue +except: + # yuul b. alwright + from queue import Queue as SimpleQueue + VER_IDP_DB = 1 VER_SESSION_DB = 1 @@ -140,13 +147,9 @@ class SvcHub(object): self.logf: Optional[typing.TextIO] = None self.logf_base_fn = "" self.is_dut = False # running in unittest; always False - self.stop_req = False self.stopping = False self.stopped = False - self.reload_req = False self.reload_mutex = threading.Lock() - self.stop_cond = threading.Condition() - self.nsigs = 3 self.retcode = 0 self.httpsrv_up = 0 self.qr_tsz = None @@ -156,6 +159,12 @@ class SvcHub(object): self.cmon = 0 self.tstack = 0.0 + self.sig_logrot = -999 + self.sig_reload = -999 + self.sig_stack = -999 + self.nsigs = 7 + self.sig = SimpleQueue() + self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) if args.sss or args.s >= 3: @@ -1451,31 +1460,37 @@ class SvcHub(object): sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: - sigs.append(signal.SIGUSR1) + sigs.append(signal.SIGHUP) + + for (opt, mem) in ( + ("logrot_sig", "sig_logrot"), + ("reload_sig", "sig_reload"), + ("stack_sig", "sig_stack"), + ): + zs = getattr(self.args, opt) + if not zs: + continue + zi = signame2int(zs) + setattr(self, mem, zi) + sigs.append(zi) for sig in sigs: signal.signal(sig, self.signal_handler) - # macos hangs after shutdown on sigterm with while-sleep, - # windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??) - # linux is fine with both, - # never lucky - if ANYWIN: - # msys-python probably fine but >msys-python - Daemon(self.stop_thr, "svchub-sig") + if self.args.sig_thr: + Daemon(self._signal_thr, "svchub-sig") try: - while not self.stop_req: + while not self.stopping: time.sleep(1) except: pass - self.shutdown() # cant join; eats signals on win10 while not self.stopped: time.sleep(0.1) else: - self.stop_thr() + self._signal_thr() def start_zeroconf(self) -> None: self.zc_ngen += 1 @@ -1533,17 +1548,6 @@ class SvcHub(object): self.asrv.load_sessions(True) self.broker.reload_sessions() - def stop_thr(self) -> None: - while not self.stop_req: - with self.stop_cond: - self.stop_cond.wait(9001) - - if self.reload_req: - self.reload_req = False - self.reload(True, True) - - self.shutdown() - def kill9(self, delay: float = 0.0) -> None: if delay > 0.01: time.sleep(delay) @@ -1556,26 +1560,42 @@ class SvcHub(object): os.kill(os.getpid(), signal.SIGKILL) def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None: - if self.stopping: - if self.nsigs <= 0: + if sig in (signal.SIGINT, signal.SIGTERM): + self.nsigs -= 1 + + if self.nsigs == 0: try: threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start() time.sleep(0.1) except: pass + if self.nsigs <= 0: self.kill9() - else: - self.nsigs -= 1 - return - if not ANYWIN and sig == signal.SIGUSR1: - self.reload_req = True + self.sig.put(sig) + + def _signal_thr(self) -> None: + while not self.stopping: + sig = self.sig.get() + self._signal_handler(sig) + + def _signal_handler(self, sig: int) -> None: + if sig == self.sig_logrot: + self.log("root", "signal: logrotate") + dt = datetime.now(self.tz) + self.logf_base_fn = "\t" + self._set_next_day(dt) + + elif sig == self.sig_reload: + self.log("root", "signal: reload") + self.reload(True, True) + + elif sig == self.sig_stack: + self.log("root", "signal: stack%s" % (alltrace(),)) + else: - self.stop_req = True - - with self.stop_cond: - self.stop_cond.notify_all() + self.shutdown() def shutdown(self) -> None: if self.stopping: @@ -1583,10 +1603,8 @@ class SvcHub(object): # start_log_thrs(print, 0.1, 1) + self.nsigs = 3 self.stopping = True - self.stop_req = True - with self.stop_cond: - self.stop_cond.notify_all() ret = 1 try: diff --git a/copyparty/util.py b/copyparty/util.py index 2d69d2fe..6396445f 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1635,6 +1635,16 @@ def expand_osenv_cs(txt) -> str: raise Exception(t) +def signame2int(txt: str) -> int: + try: + return int(txt) + except: + txt = txt.upper() + if not txt.startswith("SIG"): + txt = "SIG" + txt + return int(getattr(signal, txt)) + + def rice_tid() -> str: tid = threading.current_thread().ident c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) From 4e9ad781b693f2a9b302d6e10c023da957ec15ec Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 10:10:01 +0000 Subject: [PATCH 17/32] advisory tiers --- README.md | 13 ++++++++++--- copyparty/__main__.py | 1 + copyparty/svchub.py | 14 +++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4f887359..f487de2f 100644 --- a/README.md +++ b/README.md @@ -1336,9 +1336,15 @@ using arguments or config files, or a mix of both: sleep better at night by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories) -this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; all of them provide the same information, so which one you choose is whatever -* `https://api.copyparty.eu/advisories` -* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` +this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; choose what severity level you want to be notified for: +* `https://api.copyparty.eu/advisories-panic` -- only really bad stuff, the "UPGRADE NOW" kind +* `https://api.copyparty.eu/advisories` -- everything important / noteworthy, "upgrade when you can" +* `https://api.copyparty.eu/advisories-all` -- *everything*, including stuff that's unlikely to affect anyone +* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` -- same as `advisories-all` + +note that `https://api.copyparty.eu/advisories` may (for example) skip some advisories rated `High` but include some `Low`; that's because an easily-reachable `Low` in a default-enabled feature is more severe than a `High` which is a theoretical bug in a contrived use of a fringe feature, but the CVE calculator would still classify that as `High` + +if you want to use the github advisory feed but only care about advisories rated `medium`/`moderate` or higher, then global-option `--vc-sev medium` does that, but see previous paragraph > to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test` @@ -1354,6 +1360,7 @@ config file example: vc-url: https://api.copyparty.eu/advisories vc-age: 3 # how many hours to wait between each check vc-exit # emergency-exit if current version is vulnerable + vc-sev: medium # only care about severity 'Medium'/'Moderate' or higher (github-only; don't use this with api.copyparty.eu) ``` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8eb39cd2..666aea32 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1237,6 +1237,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("--reload-sig", metavar="S", type=u, default=("" if ANYWIN else "USR1"), help="reload server config when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR1\033[0m], [\033[32mUSR1\033[0m], [\033[32m10\033[0m]") ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)") ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks") + ap2.add_argument("--vc-sev", metavar="LEVEL", type=u, default="low", help="minimum severity to care about; one of these: \033[32mlow medium high critical\033[0m") ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable") ap2.add_argument("--license", action="store_true", help="show licenses and exit") ap2.add_argument("--version", action="store_true", help="show versions and exit") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 28f5cc0c..24f6007c 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -119,6 +119,8 @@ VER_IDP_DB = 1 VER_SESSION_DB = 1 VER_SHARES_DB = 2 +CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4} + class SvcHub(object): """ @@ -298,6 +300,9 @@ class SvcHub(object): self.log("root", "vc-age too low for copyparty.eu; will use 3 hours") args.vc_age = zi + if args.vc_sev and args.vc_sev not in CVE_SEVS: + self.log("root", "vc-sev %r invalid; will use 'low'" % (args.vc_sev,), 3) + zs = "" if args.th_ram_max < 0.22: zs = "generate thumbnails" @@ -1948,6 +1953,7 @@ class SvcHub(object): next_chk = 0 # self.args.vc_age = 2 / 60 fpath = os.path.join(self.E.cfg, "vuln_advisory.json") + minsev = CVE_SEVS.get(self.args.vc_sev, 0) while not self.stopping: now = time.time() if now < next_chk: @@ -1991,10 +1997,13 @@ class SvcHub(object): continue try: + sver = "0.1" advisories = json.loads(jtxt) for adv in advisories: if adv.get("state") == "closed": continue + if CVE_SEVS.get(adv.get("severity"), 9) < minsev: + continue vuln = {} for x in adv["vulnerabilities"]: if x["package"]["name"].lower() == "copyparty": @@ -2012,9 +2021,8 @@ class SvcHub(object): if self.args.vc_exit: self.sigterm() return - else: - t = "%sok; v%s and newer is safe" - self.log("ver-chk", t % (src, sver), 2) + t = "%sok; v%s and newer is safe" + self.log("ver-chk", t % (src, sver), 2) next_chk = time.time() + self.args.vc_age * 3600 - age except Exception as e: t = "failed to process vulnerability advisory; %s" From 926c6e814d9b50c7a5732081b5d1ca789cd17ca8 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 10:17:03 +0000 Subject: [PATCH 18/32] embedded-cover crop-control; closes #1474 --- copyparty/th_srv.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index e63ce14e..21896a04 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -230,11 +230,6 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) - if not rd: rd = "\ntop" - # spectrograms are never cropped; strip fullsize flag - ext = rem.split(".")[-1].lower() - if ext in ffa and fmt[:2] in ("wf", "jf", "xf"): - fmt = fmt.replace("f", "") - dcache = th_dir_cache rd_key = rd + "\n" + fmt rd = dcache.get(rd_key) From 27031f73be12071625c93ab231eb0235b74b2c04 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 11:08:19 +0000 Subject: [PATCH 19/32] clamp lastmod to y6325 (closes #1470); fromtimestamp fails on year>9999 --- bin/u2c.py | 7 ++++--- copyparty/httpcli.py | 2 +- copyparty/up2k.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index f0e3f8de..a6a564a1 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.20" -S_BUILD_DT = "2026-04-22" +S_VERSION = "2.21" +S_BUILD_DT = "2026-05-25" """ u2c.py: upload to copyparty @@ -1311,7 +1311,8 @@ class Ctl(object): if self.ar.jw: print("%s %s" % (wark, vp)) else: - zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC) + tsdt = datetime.datetime.fromtimestamp + zd = tsdt(max(0, min(2 << 36, file.lmod)), UTC) dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( zd.year, zd.month, diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index e4cd6a36..682721ec 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -7238,7 +7238,7 @@ class HttpCli(object): margin = "-" sz = inf.st_size - zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC) + zd = datetime.fromtimestamp(max(0, min(2 << 36, linf.st_mtime)), UTC) dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( zd.year, zd.month, diff --git a/copyparty/up2k.py b/copyparty/up2k.py index ff1ba97d..957cbc72 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3077,7 +3077,7 @@ class Up2k(object): ) zi = cj["lmod"] - bad_mt = zi <= 0 or zi > 0xAAAAAAAA + bad_mt = zi <= 0 or zi > (2 << 36) if bad_mt or vfs.flags.get("up_ts", "") == "fu": # force upload time rather than last-modified cj["lmod"] = int(time.time()) From 9068ec6a8ebbbc7f49d0a9819745f97ee3fdc9e8 Mon Sep 17 00:00:00 2001 From: Danila Kamaev Date: Mon, 25 May 2026 15:27:01 +0400 Subject: [PATCH 20/32] improve opds compatibility (#1463) use absolute paths and generate IDs --- copyparty/httpcli.py | 26 ++++++++++++++++++-------- copyparty/web/opds.xml | 4 ++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 682721ec..91a5803b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -7187,18 +7187,19 @@ class HttpCli(object): dirs = [] files = [] ptn_hr = RE_HR - use_abs_url = ( - not is_opds - and not is_ls - and not is_js - and not self.trailing_slash - and vpath + use_abs_url = is_opds or ( + vpath and not is_ls and not is_js and not self.trailing_slash ) for fn in ls_names: base = "" href = fn if use_abs_url: - base = "/" + vpath + "/" + if is_opds: + base = self.args.SRS + if vpath: + base += vpath + "/" + else: + base = "/" + vpath + "/" href = base + fn if fn in vfs_virt: @@ -7528,17 +7529,26 @@ class HttpCli(object): ] j2a["opds_osd"] = "%s%s?opds&osd" % (self.args.SRS, quotep(vpath)) - + j2a["opds_id"] = uuid.uuid5(uuid.NAMESPACE_URL, vpath + "/").urn + j2a["opds_title"] = ( + (vpath.rsplit("/", 1)[-1] + "/") if vpath else self.args.bname + ) for item in dirs: href = item["href"] href += ("&" if "?" in href else "?") + "opds" item["href"] = href + item["opds_id"] = uuid.uuid5( + uuid.NAMESPACE_URL, "%s/%s" % (vpath, item["name"]) + ).urn item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) for item in files: href = item["href"] href += ("&" if "?" in href else "?") + "dl" item["href"] = href + item["opds_id"] = uuid.uuid5( + uuid.NAMESPACE_URL, "%s/%s" % (vpath, item["name"]) + ).urn item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) if "rmagic" in self.vn.flags: diff --git a/copyparty/web/opds.xml b/copyparty/web/opds.xml index ea89edf7..d3c52d1a 100644 --- a/copyparty/web/opds.xml +++ b/copyparty/web/opds.xml @@ -1,10 +1,13 @@ + {{ opds_id }} + {{ opds_title | e }} {%- for d in dirs %} + {{ d.opds_id }} {{ d.name | e }} + {{ f.opds_id }} {{ f.name | e }} {{ f.iso8601 }} Date: Mon, 25 May 2026 13:41:24 +0000 Subject: [PATCH 21/32] PRTY_FFMPEG_BIN --- README.md | 2 ++ copyparty/mtag.py | 33 +++++++++++++++++++++------------ copyparty/th_srv.py | 22 +++++++++++----------- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index f487de2f..31d0fdc9 100644 --- a/README.md +++ b/README.md @@ -3225,6 +3225,8 @@ enable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0` to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%` +if your ffmpeg/ffprobe binaries have nonstandard names -- such as `ffmpeg8` (macports) -- set environment variables `PRTY_FFMPEG_BIN` and `PRTY_FFPROBE_BIN` to the corret name (or full path) + ### dependency chickenbits diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 65dcd9b8..c25d4538 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -46,24 +46,33 @@ except: HAVE_MUTAGEN = False -def have_ff(scmd: str) -> bool: - if ANYWIN: - scmd += ".exe" +def have_ff(name: str) -> bool: + uname = name.upper() + if os.environ.get("PRTY_NO_" + uname): + return b"" + + ebin = os.environ.get("PRTY_%s_BIN" % (uname,)) + try: + bcmd = (ebin or name).encode("utf-8") + except: + bcmd = ebin or name + + if ANYWIN and not ebin: + bcmd += b".exe" if PY2: - print("# checking {}".format(scmd)) - acmd = (scmd + " -version").encode("ascii").split(b" ") + print("# checking {}".format(bcmd)) try: - sp.Popen(acmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate() - return True + sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate() + return bcmd except: - return False + return b"" else: - return bool(shutil.which(scmd)) + return shutil.which(bcmd) or b"" -HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg") -HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe") +HAVE_FFMPEG = have_ff("ffmpeg") +HAVE_FFPROBE = have_ff("ffprobe") CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split()) CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b") @@ -219,7 +228,7 @@ def ffprobe( ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: # ffprobe -hide_banner -show_streams -show_format -- cmd = [ - b"ffprobe", + HAVE_FFPROBE, b"-hide_banner", b"-show_streams", b"-show_format", diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 21896a04..0153bf0f 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -860,7 +860,7 @@ class ThumbSrv(object): bscale = scale.format(*list(res)).encode("utf-8") # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner" @@ -1001,7 +1001,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1080,7 +1080,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1112,7 +1112,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1148,7 +1148,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1177,7 +1177,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1212,7 +1212,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1273,7 +1273,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1314,7 +1314,7 @@ class ThumbSrv(object): # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1340,7 +1340,7 @@ class ThumbSrv(object): self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6) # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", @@ -1361,7 +1361,7 @@ class ThumbSrv(object): self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6) # fmt: off cmd = [ - b"ffmpeg", + HAVE_FFMPEG, b"-nostdin", b"-v", b"error", b"-hide_banner", From 6183540c61cff4c655c625dd802efeb7b4563231 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 15:57:28 +0000 Subject: [PATCH 22/32] mde: fix crash on mobile --- .gitignore | 1 + scripts/deps-docker/Dockerfile | 1 + scripts/deps-docker/codemirror.patch | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 302136b9..4b78a4db 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ scripts/docker/base/test-aac/ scripts/docker/base/whl/ scripts/docker/i/ scripts/deps-docker/uncomment.py +scripts/deps-docker/unhint.py contrib/package/arch/pkg/ contrib/package/arch/src/ diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index a79d9426..093aa2f8 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -156,6 +156,7 @@ COPY uncomment.py unhint.py /z RUN mv /z/mfusepy-3.1.1/mfusepy.py /z/dist/ \ && cd /z/dist \ && python3 /z/unhint.py \ + && rm -f uh \ && mv mfusepy.py f1 \ && python3 /z/uncomment.py f1 \ && sed -ri '/self.__critical_exception/d; /^from (typing|collections.abc) import/d' f1 \ diff --git a/scripts/deps-docker/codemirror.patch b/scripts/deps-docker/codemirror.patch index aef47f6f..9e3bfe7f 100644 --- a/scripts/deps-docker/codemirror.patch +++ b/scripts/deps-docker/codemirror.patch @@ -114,12 +114,14 @@ diff -wNarU2 codemirror-5.65.1-orig/src/input/ContentEditableInput.js codemirror + /* let order = getOrder(line, cm.doc.direction), side = "left" if (order) { -@@ -405,4 +406,5 @@ +@@ -405,5 +406,6 @@ side = partPos % 2 ? "right" : "left" } +- let result = nodeAndOffsetInLineMap(info.map, pos.ch, side) + */ - let result = nodeAndOffsetInLineMap(info.map, pos.ch, side) ++ let result = nodeAndOffsetInLineMap(info.map, pos.ch, "left") result.offset = result.collapse == "right" ? result.end : result.start + return result diff -wNarU2 codemirror-5.65.1-orig/src/input/movement.js codemirror-5.65.1/src/input/movement.js --- codemirror-5.65.1-orig/src/input/movement.js 2022-01-20 13:06:23.000000000 +0100 +++ codemirror-5.65.1/src/input/movement.js 2022-02-09 22:50:18.145862052 +0100 From c28aa08b3513773508774464b58f5cb5bb88fdf1 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 16:12:26 +0000 Subject: [PATCH 23/32] ihead filter on ohead too --- copyparty/authsrv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index b261a1f4..2f284920 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3082,10 +3082,10 @@ class AuthSrv(object): pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x]) if pwds: if self.ah.on: - zs = r"(\[H\] %s:.*|[?&]%s=)([^&]+)" + zs = r"(\[[HO]\] %s:.*|[?&]%s=)([^&]+)" zs = zs % (self.args.pw_hdr, self.args.pw_urlp) else: - zs = r"(\[H\] %s:.*|=)(" % (self.args.pw_hdr,) + zs = r"(\[[HO]\] %s:.*|=)(" % (self.args.pw_hdr,) zs += "|".join(pwds) + r")([]&; ]|$)" self.re_pwd = re.compile(zs) From cc5420a324da9e7f0024c497189e15a7574cfb06 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 18:32:42 +0000 Subject: [PATCH 24/32] download-as-zip: toplevel optional --- README.md | 1 + copyparty/authsrv.py | 8 +------- copyparty/httpcli.py | 15 ++++++++++++--- copyparty/httpsrv.py | 2 +- copyparty/sutil.py | 12 ++++++++---- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 31d0fdc9..85802fa1 100644 --- a/README.md +++ b/README.md @@ -836,6 +836,7 @@ you can also zip a selection of files or folders by clicking them in the browser cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive * super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways +* and url-param `&name=foo` changes the name of the toplevel folder in the archive to `foo`, and just `&name` removes the folder entirely * and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms) * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0` diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 2f284920..9eb0e259 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -900,20 +900,14 @@ class VFS(object): def zipgen( self, - vpath: str, + folder: str, vrem: str, flt: set[str], uname: str, dirs: bool, dots: int, scandir: bool, - wrap: bool = True, ) -> Generator[dict[str, Any], None, None]: - - # if multiselect: add all items to archive root - # if single folder: the folder itself is the top-level item - folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top") - g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False) for _, _, vpath, apath, files, rd, vd in g: if flt: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 91a5803b..246545fc 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1823,14 +1823,13 @@ class HttpCli(object): # because lstat=true would not recurse into subfolders # and this is a rare case where we actually want that fgen = vn.zipgen( - rem, + "", rem, set(), self.uname, True, 1, not self.args.no_scandir, - wrap=False, ) elif depth == "0": @@ -5163,6 +5162,16 @@ class HttpCli(object): if items: fn = "sel-" + fn + if "name" in self.ouparam: + # user-selected name for toplevel folder, or blank for none + vpath = undot(self.ouparam["name"]) + elif items: + # multiselect; add all items to archive root + vpath = "" + else: + # single folder; the folder itself is the top-level item + vpath = vpath.split("/")[-1].lstrip(".") or "top" + if vn.flags.get("zipmax") and not ( vn.flags.get("zipmaxu") and self.uname != "*" ): @@ -5209,7 +5218,7 @@ class HttpCli(object): if cfmt: self.log("transcoding to [{}]".format(cfmt)) - fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt) + fgen = gfilter(fgen, self.thumbcli, self.uname, self.vpath, vpath, cfmt) now = time.time() self.dl_id = "%s:%s" % (self.ip, self.addr[1]) diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index f6ec1e56..db81725b 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -700,7 +700,7 @@ class HttpSrv(object): if not fmts: continue log("starting for volume /%s" % (vn.vpath,), 6) - g = vn.walk("x", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False) + g = vn.walk("", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False) g = gfilter2(g, self, vn.vpath, fmts.split(",")) for f in g: nfiles += 1 diff --git a/copyparty/sutil.py b/copyparty/sutil.py index 7fc03bab..97c10086 100644 --- a/copyparty/sutil.py +++ b/copyparty/sutil.py @@ -61,6 +61,7 @@ def gfilter( thumbcli: ThumbCli, uname: str, vtop: str, + vname: str, fmt: str, ) -> Generator[dict[str, Any], None, None]: from concurrent.futures import ThreadPoolExecutor @@ -70,7 +71,7 @@ def gfilter( _pools[tp] = 1 try: for f in fgen: - task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) + task = tp.submit(enthumb, thumbcli, uname, vtop, vname, f, fmt) pend.append((task, f)) if pend[0][0].done() or len(pend) > CORES * 4: task, f = pend.pop(0) @@ -130,7 +131,7 @@ def gfilter2( try: f = {"vp": vp, "st": fi[1]} task = tp.submit( - enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, f, fmt + enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, "", f, fmt ) pend.append((task, f)) if pend[0][0].done() or len(pend) > CORES * 4: @@ -152,14 +153,17 @@ def gfilter2( def enthumb( - thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str + thumbcli: ThumbCli, uname: str, vtop: str, vname: str, f: dict[str, Any], fmt: str ) -> dict[str, Any]: rem = f["vp"] ext = rem.rsplit(".", 1)[-1].lower() if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): raise Exception() - vp = vjoin(vtop, rem.split("/", 1)[1]) + if vname: + vp = vjoin(vtop, rem.split("/", 1)[1]) + else: + vp = vjoin(vtop, rem) vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False) dbv, vrem = vn.get_dbv(rem) thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt) From c7b80acd0d961f52b584a1788db32d162dfa8e6c Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 20:06:17 +0000 Subject: [PATCH 25/32] fix ?tar on xvol reject; would panic and kill the connection instead of just skipping the blocked symlink --- copyparty/authsrv.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 9eb0e259..4a34824f 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -627,6 +627,9 @@ class VFS(object): t = "%s has no %s in %r => %r => %r" self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6) + if not err: + return None + t = "you don't have %s-access in %r or below %r" raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath)) @@ -858,7 +861,7 @@ class VFS(object): for le in vfs_ls: ap = absreal(os.path.join(fsroot, le[0])) vn2 = self.chk_ap(ap) - if not vn2 or not vn2.get("", uname, True, False): + if not vn2 or not vn2.get("", uname, True, False, err=0): rm1.append(le) _ = [vfs_ls.remove(x) for x in rm1] # type: ignore From e32718303cd7560f60d3e0ddd473a8a69d7dfa81 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 20:16:23 +0000 Subject: [PATCH 26/32] xvol: clarify permissions --- README.md | 2 +- copyparty/__main__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85802fa1..133d962f 100644 --- a/README.md +++ b/README.md @@ -1770,7 +1770,7 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere -* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access +* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 666aea32..60bd185e 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1648,7 +1648,7 @@ def add_safety(ap): ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --no-html --no-readme --no-logues --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih") ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]") - ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") + ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user also has access (keeps permissions from the outer/initial volume) (volflag=xvol)") ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)") ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)") ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)") From f0f6933b4e5d638a5980a53d0e656e5e900f4ccb Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 17:47:08 +0000 Subject: [PATCH 27/32] bpm mtp needs f32le --- scripts/docker/base/arbeidspakke.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docker/base/arbeidspakke.sh b/scripts/docker/base/arbeidspakke.sh index a191480c..96d54930 100755 --- a/scripts/docker/base/arbeidspakke.sh +++ b/scripts/docker/base/arbeidspakke.sh @@ -48,7 +48,7 @@ sed -ri 's/\bffplay$//; s/\bsdl2-dev\b//' APKBUILD sed -ri 's/--enable-(ladspa|lv2|vaapi|vulkan)/--disable-\1/' APKBUILD sed -ri 's/--enable-lib(aom|ass|drm|fontconfig|freetype|fribidi|harfbuzz|pulse|rist|srt|ssh|v4l2|vidstab|x264|xvid|zimg|vpl)/--disable-lib\1/' APKBUILD sed -ri 's/\b(v4l-utils|libvpx)-dev\b//' APKBUILD # (try to) drop v4l2_m2m, and use builtin vp8/vp9 instead of libvpx for decode -sed -ri 's/(--disable-vulkan)/\1 --disable-devices --disable-hwaccels --disable-encoders --enable-encoder=flac --enable-encoder=libjxl --enable-encoder=libmp3lame --enable-encoder=libopus --enable-encoder=libwebp --enable-encoder=mjpeg --enable-encoder=pcm_s16le --enable-encoder=pcm_s16le_planar --enable-encoder=png --enable-encoder=rawvideo --enable-encoder=vnull --enable-encoder=wrapped_avframe --disable-muxers --enable-muxer=aiff --enable-muxer=apng --enable-muxer=caf --enable-muxer=ffmetadata --enable-muxer=fifo --enable-muxer=flac --enable-muxer=image2 --enable-muxer=image2pipe --enable-muxer=matroska --enable-muxer=matroska_audio --enable-muxer=mjpeg --enable-muxer=mp3 --enable-muxer=null --enable-muxer=opus --enable-muxer=pcm_s16le --enable-muxer=wav --enable-muxer=webm --enable-muxer=webp --enable-muxer=yuv4mpegpipe --disable-filters --enable-filter=anoisesrc --enable-filter=asplit --enable-filter=amerge --enable-filter=amix --enable-filter=aresample --enable-filter=crop --enable-filter=showspectrumpic --enable-filter=showwavespic --enable-filter=convolution --enable-filter=volume --enable-filter=compand --enable-filter=setsar --enable-filter=scale --disable-decoder=av1 --disable-hwaccel=v4l2_m2m --disable-decoder=h263_v4l2m2m --disable-decoder=h264_v4l2m2m --disable-decoder=mpeg1_v4l2m2m --disable-decoder=mpeg2_v4l2m2m --disable-decoder=mpeg4_v4l2m2m --disable-decoder=vc1_v4l2m2m --disable-decoder=vp8_v4l2m2m --disable-decoder=vp9_v4l2m2m --disable-decoder=subrip --disable-decoder=srt --disable-decoder=pgssub --disable-decoder=cc_dec --disable-decoder=dvdsub --disable-decoder=dvbsub --disable-decoder=ssa --disable-decoder=ass --disable-decoder=opus /' APKBUILD +sed -ri 's/(--disable-vulkan)/\1 --disable-devices --disable-hwaccels --disable-encoders --enable-encoder=flac --enable-encoder=libjxl --enable-encoder=libmp3lame --enable-encoder=libopus --enable-encoder=libwebp --enable-encoder=mjpeg --enable-encoder=pcm_f32le --enable-encoder=pcm_s16le --enable-encoder=pcm_s16le_planar --enable-encoder=png --enable-encoder=rawvideo --enable-encoder=vnull --enable-encoder=wrapped_avframe --disable-muxers --enable-muxer=aiff --enable-muxer=apng --enable-muxer=caf --enable-muxer=ffmetadata --enable-muxer=fifo --enable-muxer=flac --enable-muxer=image2 --enable-muxer=image2pipe --enable-muxer=matroska --enable-muxer=matroska_audio --enable-muxer=mjpeg --enable-muxer=mp3 --enable-muxer=null --enable-muxer=opus --enable-muxer=pcm_f32le --enable-muxer=pcm_s16le --enable-muxer=wav --enable-muxer=webm --enable-muxer=webp --enable-muxer=yuv4mpegpipe --disable-filters --enable-filter=anoisesrc --enable-filter=asplit --enable-filter=amerge --enable-filter=amix --enable-filter=aresample --enable-filter=crop --enable-filter=showspectrumpic --enable-filter=showwavespic --enable-filter=convolution --enable-filter=volume --enable-filter=compand --enable-filter=setsar --enable-filter=scale --disable-decoder=av1 --disable-hwaccel=v4l2_m2m --disable-decoder=h263_v4l2m2m --disable-decoder=h264_v4l2m2m --disable-decoder=mpeg1_v4l2m2m --disable-decoder=mpeg2_v4l2m2m --disable-decoder=mpeg4_v4l2m2m --disable-decoder=vc1_v4l2m2m --disable-decoder=vp8_v4l2m2m --disable-decoder=vp9_v4l2m2m --disable-decoder=subrip --disable-decoder=srt --disable-decoder=pgssub --disable-decoder=cc_dec --disable-decoder=dvdsub --disable-decoder=dvbsub --disable-decoder=ssa --disable-decoder=ass --disable-decoder=opus /' APKBUILD # `- s/av1/libdav1d/; s/libvorbis/vorbis/; s/opus/libopus/; libvorbis and mpg123 gets pulled in by openmpt } From 62dc83327328c563c4220239b317d3ac6b001b40 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 17:47:52 +0000 Subject: [PATCH 28/32] misc linter --- copyparty/broker_util.py | 2 ++ copyparty/mtag.py | 4 ++-- copyparty/svchub.py | 7 ++++++- copyparty/th_srv.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/copyparty/broker_util.py b/copyparty/broker_util.py index 10c778dc..50264e83 100644 --- a/copyparty/broker_util.py +++ b/copyparty/broker_util.py @@ -16,6 +16,7 @@ if True: # pylint: disable=using-constant-test if TYPE_CHECKING: from .httpsrv import HttpSrv + from .svchub import SvcHub class ExceptionalQueue(Queue, object): @@ -50,6 +51,7 @@ class BrokerCli(object): for example resolving httpconn.* in httpcli -- see lines tagged #mypy404 """ + hub: "SvcHub" log: "RootLogger" args: argparse.Namespace asrv: AuthSrv diff --git a/copyparty/mtag.py b/copyparty/mtag.py index c25d4538..37e83a81 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -46,7 +46,7 @@ except: HAVE_MUTAGEN = False -def have_ff(name: str) -> bool: +def have_ff(name: str) -> bytes: uname = name.upper() if os.environ.get("PRTY_NO_" + uname): return b"" @@ -55,7 +55,7 @@ def have_ff(name: str) -> bool: try: bcmd = (ebin or name).encode("utf-8") except: - bcmd = ebin or name + bcmd: bytes = ebin or name if ANYWIN and not ebin: bcmd += b".exe" diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 24f6007c..b0b1f9ef 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1477,7 +1477,12 @@ class SvcHub(object): continue zi = signame2int(zs) setattr(self, mem, zi) - sigs.append(zi) + try: + sigs.append(signal.Signals(zi)) + except: + t = "using unknown signal %r as %s" + self.log("root", t % (zi, mem), 3) + sigs.append(zi) for sig in sigs: signal.signal(sig, self.signal_handler) diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 0153bf0f..f1393a2e 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -790,7 +790,7 @@ class ThumbSrv(object): ) finally: if p and p.poll() is None: - p.kill(9) + p.kill() def _conv_rawpy(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.6, tpath) From ce33a88e252199675fd2343b2d883bce5b6a71a5 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 18:16:37 +0000 Subject: [PATCH 29/32] v1.20.15 --- copyparty/__version__.py | 4 +-- docs/changelog.md | 60 +++++++++++++++++++++++++++++++++ scripts/deps-docker/Dockerfile | 2 +- scripts/docker/Dockerfile.dj | 2 +- scripts/docker/Dockerfile.iv | 2 +- scripts/pyinstaller/deps.sha512 | 2 +- scripts/pyinstaller/notes.txt | 2 +- 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 9d2458c4..6d20349c 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 20, 14) +VERSION = (1, 20, 15) CODENAME = "sftp is fine too" -BUILD_DT = (2026, 4, 24) +BUILD_DT = (2026, 5, 26) 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 e7427dde..e2a25e8b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,63 @@ +β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ +# 2026-0424-2222 `v1.20.14` autolocalization + +## πŸ§ͺ new features + +* #1410 #376 #1224 new option `--glang` to autoselect UI-translation based on webbrowser's language (thx @stackxp!) ec3e0e7e +* #1407 #1384 option to automatically switch between list-view and grid-view depending on folder contents (thx @icxes!) 822fa718 660ed7a9 961a2737 +* #1447 audioplayer can now play bcstm / bfstm / brstm files (nintendo 3ds/wii bgm) 3a9ff67a +* #1389 add 1000-based filesize-units in addition to 1024-based 43773f2c +* #1395 [reloc-by-wark](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#more-upload-stuff), a pair of hooks to rename incoming uploads to a hash of the file contents 1e7de5d1 +* option [--rlo](https://copyparty.eu/cli/#rlo-help-page) to change the logrotate-counter for [-lo](https://copyparty.eu/cli/#g-lo) 8b986888 +* add `--certkey` to specify certificate and key as separate files 8c7cdf85 + * but the built-in HTTPS server should [still not be trusted](https://github.com/9001/copyparty/#https) +* config-files can now use OS environment-variables anywhere in the `[global]` config section cbd82b65 e52bbed8 + * by default, only the syntax `${VAR}` is supported, not `$VAR` or `%VAR%` + * previously, a small handful of global-options already supported this (`c lo hist dbpath ssl_log`), but they also supported the `$VAR` syntax, which is no longer the case + * if the old `$VAR` syntax is detected, copyparty will crash on startup, suggesting the following remedies (choose one!) in the log: + 1. update the config-value to the new `${VAR}` syntax (recommended) + 2. allow the old syntax with global-option `--env-expand 1` (risky) + 3. ignore the old syntax and only expand the new syntax with global-option `--env-expand 2` + 4. disable all environment-variable expansions with `PRTY_NO_ENVEXPAND=1` + +## 🩹 bugfixes + +* #1437 webdav clients can now PROPFIND a file with `depth: infinite` which at least [webdav4](https://github.com/skshetry/webdav4) does e00f2b46 +* #1392 navigating into a subfolder using a `dks` [dirkey](https://github.com/9001/copyparty/#dirkeys) (default-disabled) could fail 228c3dfa +* #1446 #1330 #1362 fix some small edgecases with the rightclick-menu (thx @icxes!) 874e0e7a +* #1403 #1396 audioplayer: fix ui-crash when folder contains an m3u-file and sort-order is changed during playback (thx @icxes!) 198f631a +* #1428 #1427 when `--magic` was enabled, nameless uploads of textfiles would get the file-extension `.ssa` instead of `.txt` (thx @Scotsguy!) ed516ddc +* #1449 on some filesystems, the tail/follow function would spam the log with `reopened at byte XXX` 81730189 +* #1401 on windows, a spec-violating basic-upload could delay that upload by a few seconds 6fb1287e +* on macOS, u2c would clear the terminal on exit, even with `-ns` 238887c7 +* audio-files in a videofile trenchcoat did not thumbnail correctly 1066dc39 + +## πŸ”§ other changes + +* #1387 added gentoo packaging (thx @mid-kid!) fb5384f4 +* #1425 improved FreeBSD / OpenBSD support (thx @chilledfrogs!) f5613187 745d82fa +* #1352 new handler: [fail2ban](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/404-to-fail2ban.py) (thx @Lomaiin!) 26e663d1 +* improve errormessage when the server's OS-HDD blips out of existence d1517d0c +* #1439 improve IPv6 autoban IP-range (thx @SnowSquire!) f6dc1e29 +* ensure opus transcodes will at most have 2 audio channels (stereo) b31f2902 +* #1417 smb-server: probably add IPv6 support a5d859d2 +* `--list-nics` and `--list-ips` to show autodetected network-adapters and IPs 8d4363d1 +* docs: + * nixos module-override example (thx @Scotsguy!) 0b16e875 + * make it even more obvious that `--allow-csrf` is a bad idea 9a724b01 + * mention `--urlform get` to disable message-to-serverlog ac05b4f1 + * readme: improve [shadowing](https://github.com/9001/copyparty#shadowing) phrasing 003c68d0 + * [devnotes](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#dependencies): explain the vendored dependencies 971f8ef9 + +## 🌠 fun facts + +* this release includes [code](https://github.com/9001/copyparty/commit/cbd82b65) written at [abs(unit)](https://a.ocv.me/pub/g/nerd-stuff/abs-unit.jpg) + * btw that pdp had an IPv6 lease and browsed the internet :^) + * hasn't connected to copyparty though (yet...) +* this release was powered by [一体い぀から (TaKo Hardcore bootleg)](https://soundcloud.com/takomusiccc/tako-hardcore-bootleg) followed by [Fighting My Way (YUPPUN Hardcore Remix)](https://soundcloud.com/yuppun/fightingmyway) (shd is a good dj) + + + β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ # 2026-0323-0328 `v1.20.13` dothidden diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index 093aa2f8..485461dd 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:3.23 WORKDIR /z ENV ver_hashwasm=4.12.0 \ ver_marked=4.3.0 \ - ver_dompf=3.4.1 \ + ver_dompf=3.4.6 \ ver_mde=2.18.0 \ ver_codemirror=5.65.18 \ ver_fontawesome=5.13.0 \ diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index 1d1233f0..3796722c 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -28,7 +28,7 @@ RUN apk add -U !pyc ${ADD_PKG} \ py3-wheel py3-numpy-dev \ vamp-sdk-dev \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ - && python3 -m pip install pyvips \ + && python3 -m pip install pyvips --no-build-isolation \ && bash install-deps.sh \ && apk del py3-pip .bd \ && chmod 777 /root \ diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index b85bf846..1b9e7c2a 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -17,7 +17,7 @@ RUN apk add -U !pyc ${ADD_PKG} \ vips-jxl vips-poppler vips-magick \ libraw-tools \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ - && python3 -m pip install pyvips + && python3 -m pip install pyvips --no-build-isolation COPY i innvikler.sh ./ RUN ash innvikler.sh iv diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index 9d6cdd5f..90257c9e 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -28,7 +28,7 @@ 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 -efc712162da7fb005c8869a7612d2f4983d2d073ec79e16a58e7bf1fcd01c88b1cc26656f0893c68edd2294be7c3990db2f6bd77e7e3f2613539d57994b6a033 pillow-12.1.1-cp313-cp313-win_amd64.whl +5459cfe12d953ed37c481a992e3509536b7997fbd1bb77158d3465d86d3d57af9a16fd4d695374fe6ed30cbb12ac90a2de3000dd92897ddf8bdcfc3e3de831bd pillow-12.2.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 368ea2da3e3bfe765a37c62227e84774853aaabce6954475fa45c873e5547cb5346ca03a0f6a0789af369285bb3464881fed0275a19066913d9d396d5d9b9947 python-3.13.13-amd64.exe diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 7f7b89e6..832b58a7 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -39,7 +39,7 @@ 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-12.1.1-cp313-cp313-win_amd64.whl + pillow-12.2.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.13-amd64.exe From cc80ecc3410c5b1dd783741b243deefe14f6d507 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 18:45:13 +0000 Subject: [PATCH 30/32] v1.20.16 --- copyparty/__version__.py | 2 +- copyparty/mtag.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 6d20349c..a33ecb07 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,6 +1,6 @@ # coding: utf-8 -VERSION = (1, 20, 15) +VERSION = (1, 20, 16) CODENAME = "sftp is fine too" BUILD_DT = (2026, 5, 26) diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 37e83a81..d9290783 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -53,22 +53,23 @@ def have_ff(name: str) -> bytes: ebin = os.environ.get("PRTY_%s_BIN" % (uname,)) try: - bcmd = (ebin or name).encode("utf-8") + scmd = (ebin or name).decode("utf-8") except: - bcmd: bytes = ebin or name + scmd: str = ebin or name if ANYWIN and not ebin: - bcmd += b".exe" + scmd += ".exe" if PY2: - print("# checking {}".format(bcmd)) + print("# checking %s" % (scmd,)) + bcmd = scmd.encode("utf-8") try: sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate() return bcmd except: return b"" else: - return shutil.which(bcmd) or b"" + return (shutil.which(scmd) or "").encode("utf-8") HAVE_FFMPEG = have_ff("ffmpeg") From 06af60276b6728aa8214620b8f1694896c227d91 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 18:49:07 +0000 Subject: [PATCH 31/32] update pkgs to 1.20.16 --- 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 3983488a..cabe12fc 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.20.14" +pkgver="1.20.16" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -24,7 +24,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=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12") +sha256sums=("625f95d65d95cdd6898510518d013905e6766c7d2ae0ea9ae7d5dec96e89e02d") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 770398d1..5c3ff91c 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.20.14 +pkgver=1.20.16 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -21,7 +21,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=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12") +sha256sums=("625f95d65d95cdd6898510518d013905e6766c7d2ae0ea9ae7d5dec96e89e02d") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 7ed8f8f1..c393e5d3 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.20.14/copyparty-1.20.14.tar.gz", - "version": "1.20.14", - "hash": "sha256-h4Pcg5C+F2c9MG9CTnoo3Z+bT84AXjVzTTDBspZwfBI=" + "url": "https://github.com/9001/copyparty/releases/download/v1.20.16/copyparty-1.20.16.tar.gz", + "version": "1.20.16", + "hash": "sha256-Yl+V1l2VzdaJhRBRjQE5BeZ2bH0q4Oqa59XeyW6J4C0=" } \ No newline at end of file From 6e75faa62349a59f4df328a4939ba8626d89ee1a Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 26 May 2026 19:16:31 +0000 Subject: [PATCH 32/32] docker: drop arm32 iv; deadlock on startup, probably bad cffi --- scripts/docker/make.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docker/make.sh b/scripts/docker/make.sh index 1595f91e..46987417 100755 --- a/scripts/docker/make.sh +++ b/scripts/docker/make.sh @@ -16,7 +16,7 @@ imgs="dj iv min im ac" dhub_order="iv dj min im ac" ghcr_order="ac im min dj iv" ngs=( - iv-{ppc64le,s390x} + iv-{ppc64le,s390x,arm} dj-{ppc64le,s390x,arm} )