From 198f631ac80adc33a8f072d598ff0cd1aa2b5abd Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:02:11 +0300 Subject: [PATCH 01/27] fix playlist error on re-sorted filelists (#1403) m3u files would get added (not good) --- copyparty/web/browser.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 72878b20..3f8255b9 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1719,7 +1719,9 @@ function MPlayer() { if (!tid || tid.indexOf('af-') !== 0) continue; - order.push(tid.slice(1)); + tid = tid.slice(1); + if (r.tracks[tid]) + order.push(tid); } r.order = order; r.shuffle(); From 971f8ef9440d5d6c23f005c3ce44c7172d5ddbbc Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 2 Apr 2026 20:28:16 +0000 Subject: [PATCH 02/27] devnotes: vendored deps --- docs/devnotes.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/devnotes.md b/docs/devnotes.md index 89dc8fb3..a12f43e3 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -359,6 +359,48 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it **note:** you can also just download and run [/scripts/copyparty-repack.sh](https://github.com/9001/copyparty/blob/hovudstraum/scripts/copyparty-repack.sh) -- this will grab the latest copyparty release from github and do a few repacks; works on linux/macos (and windows with msys2 or WSL) +# dependencies + +## vendored dependencies + +some third-party code has been vendored into the git repo; some for convenience, some because they have been lightly hacked to fit copyparty's usecase better: + +* inside the folder [/copyparty/stolen](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/stolen) is python-libraries which runs on the serverside: + * `surrogateescape.py` (BSD2) can be removed; only needed for python2 support + * `qrcodegen.py` (MIT) can be removed and replaced with a systemwide install of the original [qrcodegen.py](https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py); + * modifications: removed code/features that copyparty does not need/use + * `ifaddr` (BSD2) can be removed and replaced with a systemwide install of the original [ifaddr](https://github.com/ifaddr/ifaddr); + * modifications: support python2, support s390x / irix32 / graal + * `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` (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 + * [easyMDE](https://github.com/Ionaru/easy-markdown-editor/) (MIT), the alternative markdown editor, has the same [patch](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/easymde-ln.patch) to enable scroll-sync, and also some [size-golfing](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/easymde.patch) + * [codemirror5](https://github.com/codemirror/codemirror5/) (MIT) has no noteworthy changes, and has only been [size-golfed](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/codemirror.patch), could have been used as-is + * [DOMPurify](https://github.com/cure53/DOMPurify) (Apache2) is used as-is + * [hash-wasm](https://github.com/Daninet/hash-wasm/) (MIT) is used entirely as-is + * [asmcrypto.js](https://github.com/openpgpjs/asmcrypto.js/) (MIT) is abandoned software, and used almost as-is (slightly golfed for size); it is probably fine to exclude/remove this, since it will only break support for uploading from really old browsers (IE10/IE11) using up2k (the "fancy uploader") + * [prism.js](https://github.com/PrismJS/prism/) (MIT) is built with a [selection of languages](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/genprism.py); there is an assumption about the exact subset of languages elsewhere in copyparty, but there shouldn't be any big consequences of replacing it with a different build if that exists in Fedora + * an old version of [SourceCodePro](https://github.com/adobe-fonts/source-code-pro) (OFL-1.1), is size-reduced to [only the necessary characters](https://github.com/9001/copyparty/blob/41ed559faabdc180efc37fd027e7f1bb2d14d174/scripts/deps-docker/mini-fa.sh#L30-L31). There will be subtle layout issues if this is replaced with a newer version, because they changed some line-heights or something in later versions, but shouldn't be a big issue + * an old version of [font-awesome](https://github.com/FortAwesome/Font-Awesome) (OFL-1.1), size-reduced to [only the necessary icons](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/mini-fa.sh). I believe a newer version should also work. + +## optional dependencies + +explained in the [main readme](https://github.com/9001/copyparty/tree/hovudstraum#optional-dependencies), but a quick recap: + +* recommended python libraries: `argon2-cffi paramiko pyftpdlib pyopenssl pillow rawpy pyzmq` [python-magic](https://pypi.org/project/python-magic/) + * only recommended on Windows: `psutil` (not very useful on Linux) + * NOT recommended: `impacket` because the feature it enables is a security nightmare + * NOT recommended: `mutagen` because ffmpeg produces better results (albeit slower) + * NOT recommended: `pyvips` because converting to jxl is extremely RAM-heavy + * NOT recommended: `pillow-heif` due to [legal reasons](https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md) +* recommended programs: `ffmpeg ffprobe cfssl cfssljson cfssl-certinfo` + * FFmpeg powers audio transcoding, and thumbnails of formats not covered by pillow/pyvips + + # building ## dev env setup From fb5384f4122f714ed1b8d15a7e0d3da261bcdae2 Mon Sep 17 00:00:00 2001 From: mid-kid Date: Thu, 2 Apr 2026 22:36:52 +0200 Subject: [PATCH 03/27] readme: add gentoo packaging (#1387) --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 4fd5eea2..805a2fb0 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com * [packages](#packages) - the party might be closer than you think * [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/)) * [fedora package](#fedora-package) - does not exist yet + * [gentoo ::guru package](#gentoo-guru-package) - `emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU)) * [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg` * [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nixos module](#nixos-module) @@ -2585,6 +2586,23 @@ after installing, start either the system service or the user service and naviga does not exist yet; there are rumours that it is being packaged! keep an eye on this space... +## gentoo ::guru package + +`emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU)) + +but first enable the `::guru` repo; + +```bash +emerge -an app-eselect/eselect-repository +eselect repository enable guru +emerge --sync guru +``` + +to start the service as a user: +* OpenRC: `rc-service -U copyparty start && rc-update -U add copyparty default` +* systemd: [todo] + + ## homebrew formulae `brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty From d1517d0c6569f4f5bac1f90adc9d4aec8f121e12 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 4 Apr 2026 18:19:49 +0000 Subject: [PATCH 04/27] catch the server-hdd phasing out of existence (and equally unexpected stuff) --- copyparty/httpconn.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 03d33199..30d9d87d 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -22,7 +22,7 @@ from .__init__ import TYPE_CHECKING, EnvParams from .authsrv import AuthSrv # typechk from .httpcli import HttpCli from .u2idx import U2idx -from .util import HMaccas, NetMap, shut_socket +from .util import HMaccas, NetMap, min_ex, shut_socket if True: # pylint: disable=using-constant-test from typing import Optional, Pattern, Union @@ -194,12 +194,12 @@ class HttpConn(object): except Exception as ex: em = str(ex) - if "ALERT_CERTIFICATE_UNKNOWN" in em: - # android-chrome keeps doing this - pass + if "ALERT_" in em: + self.log("client refused our TLS cert or config: " + em, c=6) else: - self.log("handshake\033[0m " + em, c=5) + t = "https-handshake failed, probably due to client:\n" + self.log(t + min_ex(), c=5) return From ec3e0e7e1d322c8da29d1d9c790bbf57e821b3a8 Mon Sep 17 00:00:00 2001 From: stackxp <170874486+stackxp@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:24:11 +0200 Subject: [PATCH 05/27] add `--glang` to use browser language (#1410) Signed-off-by: ed Co-authored-by: ed --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 2 +- copyparty/web/browser.js | 48 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e0a46e79..43fc9916 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1870,6 +1870,7 @@ def add_ui(ap, retry: int): ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") + ap2.add_argument("--glang", action="store_true", help="guess the browser's default language, otherwise fall back to \033[33m--lang\033[0m") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 61cf1da9..af25c7e9 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3273,7 +3273,7 @@ class AuthSrv(object): for zs in zs.split(): if vf.get(zs): js_htm[zs] = 1 - zs = "notooltips" + zs = "glang notooltips" for zs in zs.split(): if getattr(self.args, zs, False): js_htm[zs] = 1 diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 3f8255b9..85d03ab5 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -713,6 +713,54 @@ var L = Ls[lang] || Ls.eng, LANGS = []; for (var a = 0; a < LANGN.length; a++) LANGS.push(LANGN[a][0]); +if (window.glang && navigator.languages && !/\bcplng=/.test(document.cookie)) + (function() { + var lmap = [ + ["eng", /^en/i], + ["nor", /^n[ob]/i], + ["chi", /^zh-cn/i], + ["cze", /^cs/i], + ["deu", /^de/i], + ["epo", /^eo/i], + ["fin", /^fi/i], + ["fra", /^fr/i], + ["grc", /^el/i], + ["hun", /^hu/i], + ["ita", /^it/i], + ["jpn", /^ja/i], + ["kor", /^ko/i], + ["nld", /^nl/i], + ["nno", /^nn/i], + ["pol", /^pl/i], + ["por", /^pt/i], + ["rus", /^ru/i], + ["spa", /^es/i], + ["swe", /^sv/i], + ["tur", /^tr/i], + ["ukr", /^uk/i], + ["vie", /^vi/i], + ]; + for (var a = 0; a < navigator.languages.length; a++) { + for (var b = 0; b < lmap.length; b++) { + var n = lmap[b][0]; + if (!lmap[b][1].test(navigator.languages[a]) || !has(LANGS, n)) + continue; + + if (Ls[n]) { + lang = n; + L = Ls[n]; + return; + } + if (window.stop) + window.stop(); + document.body.innerHTML = 'Loading ' + n; + setck("cplng=" + n, location.reload.bind(location)); + crashed = true; + throw 1; + } + } + })(); + function langtest() { var n = LANGS.length - 1; From ede692925edfc6fca7a52f483414d543e9303237 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 4 Apr 2026 20:28:16 +0000 Subject: [PATCH 06/27] reset language too --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8c74664b..06ea6190 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -154,7 +154,7 @@ _ = (argparse, threading) USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {} -ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() +ALL_COOKIES = "cplng cppwd cppws dots idxh js k304 no304".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" From ed516ddc20493e670c141c4a3811f93632be046e Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Fri, 10 Apr 2026 01:20:53 +0200 Subject: [PATCH 07/27] make `.txt` the default extension for `text/plain` (#1428) `MIMES.items()` iterates in insertion order, so the last-inserted entry had priority, meaning that `text/plain` got `ssa` as default extension. --- copyparty/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/copyparty/util.py b/copyparty/util.py index 656a329f..9343f01d 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -491,13 +491,12 @@ font woff woff2 otf ttf for v in vs.strip().split(): MIMES[v] = "{}/{}".format(k, v) - for ln in """text md=plain txt=plain js=javascript + for ln in """text md=plain js=javascript ass=plain ssa=plain txt=plain application 7z=x-7z-compressed tar=x-tar bz2=x-bzip2 gz=gzip rar=x-rar-compressed zst=zstd xz=x-xz lz=lzip cpio=x-cpio application msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension application epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent application p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3 -text ass=plain ssa=plain image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu 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 From 822fa718004b598228171a5eecf4c23bc178ddf5 Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:33:28 +0300 Subject: [PATCH 08/27] add autogrid (#1407) Co-authored-by: ed --- copyparty/web/browser.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 85d03ab5..6efd303c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -228,6 +228,7 @@ if (1) "cl_hpick": "tap on column headers to hide in the table below", "cl_hcancel": "column hiding aborted", "cl_rcm": "right-click menu", + "cl_gauto": "autogrid", "ct_grid": '田 the grid', "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', @@ -282,6 +283,8 @@ if (1) "tt_dynt": "autogrow as tree expands", "tt_wrap": "word wrap", "tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N  cursor is in the left gutter )", + "tt_gauto": "display as grid or list depending on folder contents", + "tt_gathr": "use grid if this percentage of files are pics/vids", "ml_pmode": "at end of folder...", "ml_btns": "cmds", @@ -984,6 +987,13 @@ ebi('op_cfg').innerHTML = ( ' \n' + '\n' + '
\n' + + '

' + L.cl_gauto + '

\n' + + '
\n' + + ' ' + L.enable + '\n' + + ' ' + + '
\n' + + '
\n' + + '
\n' + '

' + L.cl_hfsz + '

\n' + '
'); QS("#files tbody").appendChild(row); - } + } else { var row = mknod('a', 'rcm_tmp', ''); From 961a273764fc6afadd2bd3765a7bbae7b9961832 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 10 Apr 2026 19:37:00 +0000 Subject: [PATCH 12/27] autogrid global-option --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 1 + copyparty/web/browser.js | 6 +++--- tests/util.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 43fc9916..286e358f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1868,6 +1868,7 @@ def add_ui(ap, retry: int): ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") + ap2.add_argument("--gauto", metavar="PERCENT", type=int, default=0, help="switch to gridview if more than \033[33mPERCENT\033[0m of files are pics/vids; 0=disabled") ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--glang", action="store_true", help="guess the browser's default language, otherwise fall back to \033[33m--lang\033[0m") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index af25c7e9..40d5944e 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3258,6 +3258,7 @@ class AuthSrv(object): "idxh": int(self.args.ih), "dutc": not self.args.localtime, "dfszf": self.args.ui_filesz.strip("-"), + "dgauto": self.args.gauto, "themes": self.args.themes, "turbolvl": self.args.turbo, "nosubtle": self.args.nosubtle, diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index b525dc29..1efb295e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -2,7 +2,7 @@ var J_BRW = 1; -if (window.rw_edit === undefined) +if (window.dgauto === undefined) alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser'); var XHR = XMLHttpRequest; @@ -6015,13 +6015,13 @@ var thegrid = (function () { pbar.onresize(); vbar.onresize(); }); - bcfg_bind(r, 'gaen', 'gauto', false, function(v) { + bcfg_bind(r, 'gaen', 'gauto', !!dgauto, function(v) { if (r.en && sread("griden") != 1) { r.en = false; r.setvis(true); } }); - ebi('ga_thresh').value = r.gathr = icfg_get('ga_thresh', 70); + ebi('ga_thresh').value = r.gathr = icfg_get('ga_thresh', dgauto || 70); ebi('ga_thresh').oninput = function (e) { var n = parseInt(this.value); swrite('ga_thresh', r.gathr = (isNum(n) ? n : 0) || 70); diff --git a/tests/util.py b/tests/util.py index 802417c5..16701876 100644 --- a/tests/util.py +++ b/tests/util.py @@ -161,7 +161,7 @@ class Cfg(Namespace): ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv th_qvx ups_who ver_iwho zip_who" ka.update(**{k: 9 for k in ex.split()}) - ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" + ex = "ctl_re db_act forget_ip gauto idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) ex = "ah_alg bname chdir chmod_f chpw_db db_xattr doctitle df epilogues exit favico fika ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr shr1 shr_site site smsg tcolor textfiles th_pregen txt_eol ufavico ufavico_h unlist up_site vc_url vname xff_src zipmaxt R RS SR" From 003c68d02739f3b90de3a02ceb9b3a7afa42311f Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 10 Apr 2026 19:52:43 +0000 Subject: [PATCH 13/27] readme: shadowing --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 805a2fb0..bb8406a9 100644 --- a/README.md +++ b/README.md @@ -608,10 +608,12 @@ and if you want to use config files instead of commandline args (good!) then her hiding specific subfolders by mounting another volume on top of them -for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty` +for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty` the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead +so, to shadow a file/folder, define a volume but leave out the `accs:` section + > ℹ️ this also works for single files, because files can also be volumes From f5613187b44dbef8c36c5a35e70eaa175d622354 Mon Sep 17 00:00:00 2001 From: chilledfrogs Date: Fri, 10 Apr 2026 22:02:01 +0200 Subject: [PATCH 14/27] improve *BSD compat (#1425) reuse some macOS stuff since lsblk and /proc doesn't apply to *BSD --- copyparty/__init__.py | 8 ++++++++ copyparty/fsutil.py | 8 ++++---- copyparty/tcpsrv.py | 9 ++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 55befeb7..8bffed86 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -44,6 +44,14 @@ ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"] MACOS = platform.system() == "Darwin" +FREEBSD = platform.system() == "FreeBSD" + +OPENBSD = platform.system() == "OpenBSD" + +ANYBSD = FREEBSD or OPENBSD + +UNIX = MACOS or ANYBSD + GRAAL = platform.python_implementation() == "GraalVM" EXE = bool(getattr(sys, "frozen", False)) diff --git a/copyparty/fsutil.py b/copyparty/fsutil.py index 9b2540c2..b0b1e8a4 100644 --- a/copyparty/fsutil.py +++ b/copyparty/fsutil.py @@ -7,7 +7,7 @@ import os import re import time -from .__init__ import ANYWIN, MACOS +from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX from .authsrv import AXS, VFS, AuthSrv from .bos import bos from .util import chkcmd, json_hesc, min_ex, undot @@ -88,7 +88,7 @@ class Fstab(object): def _from_sp_mount(self) -> dict[str, str]: sptn = r"^.*? on (.*) type ([^ ]+) \(.*" - if MACOS: + if MACOS or FREEBSD: sptn = r"^.*? on (.*) \(([^ ]+), .*" ptn = re.compile(sptn) @@ -118,7 +118,7 @@ class Fstab(object): def build_tab(self) -> None: self.log("inspecting mtab for changes") - dtab = self._from_sp_mount() if MACOS else self._from_proc() + dtab = self._from_sp_mount() if UNIX else self._from_proc() # keep empirically-correct values if mounttab unchanged srctab = str(sorted(dtab.items())) @@ -130,7 +130,7 @@ class Fstab(object): try: fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"] - if not fuses or MACOS: + if not fuses or UNIX: raise Exception() try: so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6 diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index c28406bf..c98b1d3b 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -7,7 +7,7 @@ import socket import sys import time -from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode +from .__init__ import ANYWIN, OPENBSD, PY2, TYPE_CHECKING, UNIX, unicode from .cert import gencert from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen from .util import ( @@ -510,6 +510,13 @@ class TcpSrv(object): return eps def _extdevs_nix(self) -> Generator[str, None, None]: + if UNIX: + so, _ = chkcmd(["netstat", "-nrf", "inet"]) + for ln in so.split("\n"): + if not ln.startswith("default"): + continue + yield ln.split()[7] if OPENBSD else ln.split()[3] + return with open("/proc/net/route", "rb") as f: next(f) for ln in f: From 0b16e875da401e1168cb571e44f4674cdc440a09 Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Fri, 10 Apr 2026 22:02:50 +0200 Subject: [PATCH 15/27] nixos: add override example (#1406) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bb8406a9..625ff72c 100644 --- a/README.md +++ b/README.md @@ -2749,6 +2749,12 @@ services.copyparty = { }; # you may increase the open file limit for the process openFilesLimit = 8192; + + # override the package used by the module to add dependencies, e.g. for hooks + package = pkgs.copyparty.override { + # provides exiftool for bin/hooks/image-noexif.py + extraPackages = [ pkgs.exiftool ]; + }; }; ``` From a5d859d2b18f53ccf236bc6229856f79139d531c Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 11 Apr 2026 00:17:05 +0000 Subject: [PATCH 16/27] smb: add ipv6 support; closes #1417 --- copyparty/__main__.py | 1 + copyparty/smbd.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 286e358f..727404f1 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1520,6 +1520,7 @@ def add_smb(ap): ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance") ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs") ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)") + ap2.add_argument("--smb6", action="store_true", help="enable IPv6") ap2.add_argument("--smbv", action="store_true", help="verbose") ap2.add_argument("--smbvv", action="store_true", help="verboser") ap2.add_argument("--smbvvv", action="store_true", help="verbosest") diff --git a/copyparty/smbd.py b/copyparty/smbd.py index e0ca920c..3ae41091 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -89,13 +89,15 @@ class SMB(object): smbserver.isInFileJail = self._is_in_file_jail self._disarm() - ip = next((x for x in self.args.smb_i if ":" not in x), None) + zs = " " if self.args.smb6 else ":" + ip = next((x for x in self.args.smb_i if zs not in x), None) if not ip: - self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3) + self.log("smb", "IPv6 not enabled with --smb6; listening on 0.0.0.0", 3) ip = "0.0.0.0" port = int(self.args.smb_port) - srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port) + kw = {"ipv6": True} if ":" in ip else {} + srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port, **kw) try: if self.accs: srv.setAuthCallback(self._auth_cb) @@ -121,6 +123,7 @@ class SMB(object): self.srv = srv self.stop = srv.stop + ip = "[%s]" % (ip,) if kw else ip self.log("smb", "listening @ {}:{}".format(ip, port)) def nlog(self, msg: str, c: Union[int, str] = 0) -> None: From d93fadd87ed34f3e578472e1a593e6c744005898 Mon Sep 17 00:00:00 2001 From: /dev/urandom <53902042+slashdevslashurandom@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:13:37 +0300 Subject: [PATCH 17/27] esperanto fixes (#1435) --- copyparty/web/tl/epo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index 0031a7eb..8280b4be 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -221,7 +221,7 @@ Ls.epo = { "cl_hpick": "alklaki la kapojn de kolumnoj por kasi en la suban tabelon", "cl_hcancel": "kaŝado de kolumno nuligita", "cl_rcm": "dekstra-klaka menuo", - "cl_gauto": "aŭto田", //m + "cl_gauto": "aŭto田", "ct_grid": '田 krado', "ct_ttips": '◔ ◡ ◔">ℹ️ ŝpruchelpiloj', @@ -276,8 +276,8 @@ Ls.epo = { "tt_dynt": "aŭtomate pligrandigi panelon", "tt_wrap": "linifaldo", "tt_hover": "montri kompletajn nomojn sur musumo$N( paneas rulumadon, se la kursoro de muso $N  ne estas en la maldekstra malplenaĵo )", - "tt_gauto": "montri kiel krado aŭ listo laŭ dosieruja enhavo", //m - "tt_gathr": "uzi kradon se ĉi tiu procento de dosieroj estas bildoj/filmetoj", //m + "tt_gauto": "montri kiel krado aŭ listo laŭ dosieruja enhavo", + "tt_gathr": "uzi kradon se ĉi tiu elcento da dosieroj estas bildoj/filmetoj", "ml_pmode": "je la fino de dosierujo...", "ml_btns": "komandoj", From f6dc1e2996c8aa19bcbe666a345bf4e4e9f8eea0 Mon Sep 17 00:00:00 2001 From: Snoww <40239844+SnowSquire@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:14:35 -0400 Subject: [PATCH 18/27] better ipv6 ratelimiting logic (#1439) aligns ipv6 normalizatoin logic with the typical residental allocation of /56 instead of /64. --- copyparty/util.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/copyparty/util.py b/copyparty/util.py index 9343f01d..b0004e5a 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1501,8 +1501,7 @@ class Garda(object): return 0, ip if ":" in ip: - # assume /64 clients; drop 4 groups - ip = IPv6Address(ip).exploded[:-20] + ip = ipnorm(ip) if prev and self.uniq: if self.prev.get(ip) == prev: @@ -2445,8 +2444,8 @@ def odfusion( def ipnorm(ip: str) -> str: if ":" in ip: - # assume /64 clients; drop 4 groups - return IPv6Address(ip).exploded[:-20] + # assume /56 clients; drop final 72 bits + return str(IPv6Network(ip + "/56", strict=False).network_address) return ip From 745d82faf8f3234a1aabc4636b1eff2a628ffb84 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 15 Apr 2026 23:45:22 +0200 Subject: [PATCH 19/27] tests: support freebsd --- README.md | 2 +- tests/util.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 625ff72c..b2899f80 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by * **Alpine:** `apk add py3-pillow ffmpeg` * **Debian:** `apt install --no-install-recommends python3-pil ffmpeg` * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing` -* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg` +* **FreeBSD:** `pkg install py311-sqlite3 py311-pillow ffmpeg` * **MacOS:** `port install py-Pillow ffmpeg` * **MacOS** (alternative): `brew install pillow ffmpeg` * **Windows:** `python -m pip install --user -U Pillow` diff --git a/tests/util.py b/tests/util.py index 16701876..dd10201d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -78,8 +78,10 @@ def get_ramdisk(): return ret for vol in ["/dev/shm", "/Volumes/cptd"]: # nosec (singleton test) - if os.path.exists(vol): + try: return subdir(vol) + except: + pass if os.path.exists("/Volumes"): sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From e00f2b46eb41e074ea82014b04f67d83a4ecf0aa Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 16 Apr 2026 10:01:22 +0000 Subject: [PATCH 20/27] webdav: allow propfind depth:inf on files (closes #1437); some webdav-clients (webdav4) sends Depth: infinite also for files, assume Depth: 0 in this case --- copyparty/httpcli.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 06ea6190..79186d42 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1800,7 +1800,11 @@ class HttpCli(object): topdir = {"vp": "", "st": st} fgen: Iterable[dict[str, Any]] = [] - depth = self.headers.get("depth", "infinity").lower() + if stat.S_ISDIR(st.st_mode): + depth = self.headers.get("depth", "infinity").lower() + else: + depth = "0" + if depth == "infinity": # allow depth:0 from unmapped root, but require read-axs otherwise if not self.can_read and (self.vpath or self.asrv.vfs.realpath): @@ -1809,12 +1813,6 @@ class HttpCli(object): self.log(t, 3) raise Pebkac(401, t) - if not stat.S_ISDIR(topdir["st"].st_mode): - t = "depth:infinity can only be used on folders; %r is 0o%o" - t = t % ("/" + self.vpath, topdir["st"]) - self.log(t, 3) - raise Pebkac(400, t) - if not self.args.dav_inf: self.log("client wants --dav-inf", 3) zb = b'\n' @@ -1835,7 +1833,7 @@ class HttpCli(object): wrap=False, ) - elif depth == "0" or not stat.S_ISDIR(st.st_mode): + elif depth == "0": if depth == "0" and not self.vpath and not vn.realpath: # rootless server; give dummy listing self.can_read = True From 6fb1287e7fddfbc5011299baa4c56ce8db5ec2ae Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 16 Apr 2026 11:03:51 +0000 Subject: [PATCH 21/27] nameless uploads skip hooks+up2k; closes #1401 --- copyparty/httpcli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 79186d42..3dcc2085 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3719,12 +3719,12 @@ class HttpCli(object): fdir = fdir_base fname = sanitize_fn(p_file or "") - abspath = os.path.join(fdir, fname) suffix = "-%.6f-%s" % (time.time(), dip) if p_file and not nullwrite: if rnd: fname = rand_name(fdir, fname, rnd) + abspath = os.path.join(fdir, fname) open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags} if "replace" in self.uparam or "replace" in self.headers: @@ -3742,7 +3742,7 @@ class HttpCli(object): tnam = fname = os.devnull fdir = abspath = "" - if xbu: + if xbu and abspath: at = time.time() - lifetime hr = runhook( self.log, @@ -3790,7 +3790,7 @@ class HttpCli(object): else: open_args["fdir"] = fdir - if p_file and not nullwrite: + if abspath: bos.makedirs(fdir, vf=vfs.flags) # reserve destination filename @@ -3844,9 +3844,15 @@ class HttpCli(object): fname = os.devnull raise - if not nullwrite: - atomic_move(self.log, tabspath, abspath, vfs.flags) + self.conn.nbyte += sz + if not abspath: + files.append( + (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, "") + ) + tabspath = "" + continue + atomic_move(self.log, tabspath, abspath, vfs.flags) tabspath = "" at = time.time() - lifetime @@ -3901,9 +3907,7 @@ class HttpCli(object): abspath = ap2 sz = bos.path.getsize(abspath) - files.append( - (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, abspath) - ) + files.append((sz, sha_hex, sha_b64, p_file, fname, abspath)) dbv, vrem = vfs.get_dbv(rem) self.conn.hsrv.broker.say( "up2k.hash_file", @@ -3917,7 +3921,6 @@ class HttpCli(object): self.uname, True, ) - self.conn.nbyte += sz except Pebkac: self.parser.drop() From a997455b5a3d937f53ad40f431534a0e3865e9f7 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 16 Apr 2026 11:38:06 +0000 Subject: [PATCH 22/27] bup: skip lim for nullwrite/nameless (perf) --- copyparty/httpcli.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 3dcc2085..fab99948 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3828,6 +3828,14 @@ class HttpCli(object): finally: f.close() + self.conn.nbyte += sz + if not abspath: + files.append( + (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, "") + ) + tabspath = "" + continue + if lim: lim.nup(self.ip) lim.bup(self.ip, sz) @@ -3838,20 +3846,11 @@ class HttpCli(object): lim.chk_bup(self.ip) lim.chk_nup(self.ip) except: - if not nullwrite: - wunlink(self.log, tabspath, vfs.flags) - wunlink(self.log, abspath, vfs.flags) + wunlink(self.log, tabspath, vfs.flags) + wunlink(self.log, abspath, vfs.flags) fname = os.devnull raise - self.conn.nbyte += sz - if not abspath: - files.append( - (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, "") - ) - tabspath = "" - continue - atomic_move(self.log, tabspath, abspath, vfs.flags) tabspath = "" From 874e0e7a01cf4d1210c37b1e3b87831eb63b6a89 Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:44:32 +0300 Subject: [PATCH 23/27] fix rcm related errors (#1446) fix error when rightclicking certain elements, and disable hotkey ^A in inputboxes Co-authored-by: ed --- copyparty/web/browser.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 1efb295e..89270d54 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -5740,8 +5740,10 @@ var thegrid = (function () { var ths = QSA('#ggrid>a'); for (var a = 0, aa = ths.length; a < aa; a++) { - var tr = ebi(ths[a].getAttribute('ref')).closest('tr'), - cl = tr.className || ''; + var ref = ths[a].getAttribute('ref'); + if (!ref) + continue; + var cl = ebi(ref).closest('tr').className || ''; if (noq_href(ths[a]).endsWith('/')) cl += ' dir'; @@ -6270,6 +6272,9 @@ var ahotkeys = function (e) { return ebi('griden').click(); } + if (aet == 'input') + return; + var in_ftab = (aet == 'tr' || aet == 'td') && ae.closest('#files'); if (in_ftab) { var d = '', rem = 0; From 3a9ff67ab06c168bd6d439a0c07900f3fcfde9ac Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 20 Apr 2026 20:31:04 +0200 Subject: [PATCH 24/27] audioplayer: add bcstm/bfstm/brstm; closes #1447 --- copyparty/__main__.py | 2 +- copyparty/web/browser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 727404f1..030adfeb 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1741,7 +1741,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,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,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/web/browser.js b/copyparty/web/browser.js index 89270d54..84323b63 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1697,7 +1697,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|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|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 b31f29024a55d06c1bf26273e41fddb2c3b87457 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 20 Apr 2026 20:39:14 +0200 Subject: [PATCH 25/27] audioplayer: opus: enforce 1ch or 2ch; downmix 3+ --- copyparty/mtag.py | 1 + copyparty/th_srv.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 1d4df60e..38caeb21 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -216,6 +216,7 @@ def au_unpk( def ffprobe( abspath: str, timeout: int = 60 ) -> 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", b"-hide_banner", diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 93ce8c22..eb039e3f 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -1219,6 +1219,13 @@ class ThumbSrv(object): self.log("conv2 %s [%s]" % (container, enc), 6) benc = enc.encode("ascii").split(b" ") + ac = b"2" + try: + if tags["chs"][1] in ("mono", "1", "1.0"): + ac = b"1" + except: + pass + # fmt: off cmd = [ b"ffmpeg", @@ -1228,6 +1235,7 @@ class ThumbSrv(object): b"-i", fsenc(abspath), ] + tagset + [ b"-map", b"0:a:0", + b"-ac", ac, ] + benc + [ b"-f", container, fsenc(tpath) @@ -1268,6 +1276,7 @@ class ThumbSrv(object): b"-i", fsenc(abspath), b"-map_metadata", b"-1", b"-map", b"0:a:0", + b"-ac", b"2", ] + benc + [ b"-f", b"opus", fsenc(tmp_opus) From cbd82b654a1431a076c3fe6a0ec653dca2500b57 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 20 Apr 2026 23:43:00 +0200 Subject: [PATCH 26/27] use ${ENV} syntax for env-vars; only expand environment-variables of the form ${ENV} by default, crash on startup if the old $ENV syntax is found, explaning that the old syntax can be enabled with an option --- README.md | 1 + contrib/podman-systemd/copyparty.conf | 2 +- contrib/systemd/copyparty.example.conf | 2 +- copyparty/__main__.py | 33 +++++++++++++++++++-- copyparty/authsrv.py | 28 ++++++++++-------- copyparty/mtag.py | 3 +- copyparty/svchub.py | 10 +++++-- copyparty/util.py | 41 ++++++++++++++++++++++++-- tests/util.py | 4 ++- 9 files changed, 101 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b2899f80..c8f01048 100644 --- a/README.md +++ b/README.md @@ -2542,6 +2542,7 @@ buggy feature? rip it out by setting any of the following environment variables | -------------------- | ------------ | | `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 | | `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access | +| `PRTY_NO_ENVEXPAND` | do not expand environment-variables in configs and args | | `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes | | `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` | | `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) | diff --git a/contrib/podman-systemd/copyparty.conf b/contrib/podman-systemd/copyparty.conf index aa3d4b2e..001835c6 100644 --- a/contrib/podman-systemd/copyparty.conf +++ b/contrib/podman-systemd/copyparty.conf @@ -8,7 +8,7 @@ # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (note: enable compression by adding .xz at the end) - # q, lo: $LOGS_DIRECTORY/%Y-%m%d.log + # q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log # p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE) # i: 127.0.0.1 # only allow connections from localhost (reverse-proxies) diff --git a/contrib/systemd/copyparty.example.conf b/contrib/systemd/copyparty.example.conf index de220e9f..b85ec770 100644 --- a/contrib/systemd/copyparty.example.conf +++ b/contrib/systemd/copyparty.example.conf @@ -16,7 +16,7 @@ # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (note: enable compression by adding .xz at the end) - q, lo: $LOGS_DIRECTORY/%Y-%m%d.log + q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log # enable version-checker by uncommenting one of the 'vc-url' lines below; this will # periodically check if your copyparty version has a known security vulnerability, diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 030adfeb..5c85a5b2 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -65,6 +65,10 @@ from .util import ( b64enc, ctypes, dedent, + expand_osenv_c, + expand_osenv_cs, + expand_osenv_noop, + expand_osenv_s, has_resource, load_resource, min_ex, @@ -427,9 +431,22 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None: sys.exit(0) +def expand_cvars(argv) -> list[str]: + n = 0 + for v in argv: + if "=" in v: + a, b = v.split("=", 1) + v = "%s=%s" % (a, os.path.expanduser(expand_osenv_c(b))) + else: + v = os.path.expanduser(expand_osenv_c(v)) + argv[n] = v + n += 1 + return argv + + def args_from_cfg(cfg_path: str) -> list[str]: lines: list[str] = [] - expand_config_file(None, lines, cfg_path, "") + expand_config_file(None, expand_osenv_c, lines, cfg_path, "") lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "") ret: list[str] = [] @@ -453,10 +470,12 @@ def args_from_cfg(cfg_path: str) -> list[str]: else: ret.append(prefix + k + "=" + v) - return ret + return expand_cvars(ret) def expand_cfg(argv) -> list[str]: + argv = expand_cvars(argv) + if CFG_DEF: supp = args_from_cfg(CFG_DEF[0]) argv = argv[:1] + supp + argv[1:] @@ -1197,6 +1216,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)") ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS) ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]") + ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="syntax to expect for environment-variables to expand in config-files; [\033[32m0\033[0m]=disable, [\033[32m1\033[0m]=$VAR (old syntax (scary)), [\033[32m2\033[0m]=${VAR} (new syntax (recommended))") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") 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)") @@ -2179,6 +2199,15 @@ def main(argv: Optional[list[str]] = None) -> None: quotecheck(al) + if al.env_expand == 2: + al.shenvexp = expand_osenv_c + elif al.env_expand == 1: + al.shenvexp = expand_osenv_s + elif al.env_expand == 0: + al.shenvexp = expand_osenv_noop + else: + al.shenvexp = expand_osenv_cs + if al.chdir: os.chdir(al.chdir) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 40d5944e..7dde17f4 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -56,7 +56,7 @@ if HAVE_SQLITE3: if True: # pylint: disable=using-constant-test from collections.abc import Iterable - from typing import Any, Generator, Optional, Sequence, Union + from typing import Any, Callable, Generator, Optional, Sequence, Union from .util import NamedLogger, RootLogger @@ -541,13 +541,11 @@ class VFS(object): hist = flags.get("hist") if hist and hist != "-": - zs = "{}/{}".format(hist.rstrip("/"), name) - flags["hist"] = os.path.expandvars(os.path.expanduser(zs)) + flags["hist"] = "%s/%s" % (hist.rstrip("/"), name) dbp = flags.get("dbpath") if dbp and dbp != "-": - zs = "{}/{}".format(dbp.rstrip("/"), name) - flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs)) + flags["dbpath"] = "%s/%s" % (dbp.rstrip("/"), name) return flags @@ -1279,7 +1277,7 @@ class AuthSrv(object): daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], ) -> tuple[str, str]: - src = os.path.expandvars(os.path.expanduser(src)) + src = os.path.expanduser(self.args.shenvexp(src)) src = absreal(src) dst = dst.strip("/") @@ -1372,7 +1370,7 @@ class AuthSrv(object): ) -> None: self.line_ctr = 0 - expand_config_file(self.log, cfg_lines, fp, "") + expand_config_file(self.log, self.args.shenvexp, cfg_lines, fp, "") if self.args.vc: lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)] self.log("expanded config file (unprocessed):\n" + "\n".join(lns)) @@ -2174,7 +2172,7 @@ class AuthSrv(object): if vflag == "-": pass elif vflag: - vflag = os.path.expandvars(os.path.expanduser(vflag)) + vflag = os.path.expanduser(self.args.shenvexp(vflag)) vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag elif self.args.hist: for nch in range(len(hid)): @@ -2209,7 +2207,7 @@ class AuthSrv(object): if vflag == "-": pass elif vflag: - vflag = os.path.expandvars(os.path.expanduser(vflag)) + vflag = os.path.expanduser(self.args.shenvexp(vflag)) vol.dbpath = uncyg(vflag) if WINDOWS else vflag elif self.args.dbpath: for nch in range(len(hid)): @@ -3964,10 +3962,14 @@ def split_cfg_ln(ln: str) -> dict[str, Any]: def expand_config_file( - log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str + log: Optional["NamedLogger"], + shenvexp: "Callable[[str], str]", + ret: list[str], + fp: str, + ipath: str, ) -> None: """expand all % file includes""" - fp = absreal(fp) + fp = absreal(os.path.expanduser(shenvexp(fp))) if len(ipath.split(" -> ")) > 64: raise Exception("hit max depth of 64 includes") @@ -3998,7 +4000,7 @@ def expand_config_file( if fp2 in ipath: continue - expand_config_file(log, ret, fp2, ipath) + expand_config_file(log, shenvexp, ret, fp2, ipath) return @@ -4023,7 +4025,7 @@ def expand_config_file( fp2 = ln[1:].strip() fp2 = os.path.join(os.path.dirname(fp), fp2) ofs = len(ret) - expand_config_file(log, ret, fp2, ipath) + expand_config_file(log, shenvexp, ret, fp2, ipath) for n in range(ofs, len(ret)): ret[n] = pad + ret[n] continue diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 38caeb21..65dcd9b8 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -17,6 +17,7 @@ from .util import ( FFMPEG_URL, REKOBO_LKEY, VF_CAREFUL, + expand_osenv_c, fsenc, gzip, min_ex, @@ -86,7 +87,7 @@ class MParser(object): while True: try: - bp = os.path.expanduser(args) + bp = os.path.expanduser(expand_osenv_c(args)) if WINDOWS: bp = uncyg(bp) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index d5a59722..5f7441c3 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1133,17 +1133,23 @@ class SvcHub(object): al.th_coversd_set = set(al.th_coversd) for k in "c".split(" "): + if self.args.env_expand in (0, 2): + break + vl = getattr(al, k) if not vl: continue - vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl] + vl = [os.path.expanduser(self.args.shenvexp(x)) for x in vl] setattr(al, k, vl) for k in "lo hist dbpath ssl_log".split(" "): + if self.args.env_expand in (0, 2): + break + vs = getattr(al, k) if vs: - vs = os.path.expandvars(os.path.expanduser(vs)) + vs = os.path.expanduser(self.args.shenvexp(vs)) setattr(al, k, vs) for k in "idp_adm stats_u".split(" "): diff --git a/copyparty/util.py b/copyparty/util.py index b0004e5a..c8a8dfc1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1562,6 +1562,43 @@ def dedent(txt: str) -> str: return "\n".join([ln[pad:] for ln in lns]) +def expand_osenv_noop(txt) -> str: + return txt + + +def _expand_osenv_c(txt) -> str: + if "${" not in txt: + return txt + zsl = txt.split("${") + ret = zsl[0] + for v in zsl[1:]: + if "}" not in v: + raise Exception("missing '}' after %r in config-value %r" % (v, txt)) + a, b = v.split("}", 1) + try: + ret += os.environ[a] + b + except: + raise Exception("env-var %r not defined; config-value %r" % (a, txt)) + return ret + + +if os.environ.get("PRTY_NO_ENVEXPAND"): + expand_osenv_c = expand_osenv_noop + expand_osenv_s = expand_osenv_noop +else: + expand_osenv_c = _expand_osenv_c + expand_osenv_s = os.path.expandvars + + +def expand_osenv_cs(txt) -> str: + a = expand_osenv_c(txt) + b = expand_osenv_s(txt) + if a == b: + return a + t = "config-value %r is using the old syntax for environment-variables; choose one of the following options:\noption 1: update the config-value to the new syntax, ${VAR} instead of $VAR or %%VAR%%\noption 2: tell copyparty to allow the old syntax with global-option --env-expand 1 (risky)\noption 3: tell copyparty to only use the new syntax (and not expand this variable) with global-option --env-expand 2\noption 4: disable all environment-variable expansions with PRTY_NO_ENVEXPAND=1 or global-option --env-expand 0" + raise Exception(t % (txt,)) + + def rice_tid() -> str: tid = threading.current_thread().ident c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) @@ -3847,7 +3884,7 @@ def _parsehook( argv = cmd.split(",") if "," in cmd else [cmd] - argv[0] = os.path.expandvars(os.path.expanduser(argv[0])) + argv[0] = os.path.expanduser(expand_osenv_c(argv[0])) return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv @@ -4190,7 +4227,7 @@ def loadpy(ap: str, hot: bool) -> Any: depending on what other inconveniently named files happen to be in the same folder """ - ap = os.path.expandvars(os.path.expanduser(ap)) + ap = os.path.expanduser(expand_osenv_c(ap)) mdir, mfile = os.path.split(absreal(ap)) mname = mfile.rsplit(".", 1)[0] sys.path.insert(0, mdir) diff --git a/tests/util.py b/tests/util.py index dd10201d..6cdb7ed5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -38,7 +38,7 @@ from copyparty.broker_thr import BrokerThr from copyparty.ico import Ico from copyparty.u2idx import U2idx from copyparty.up2k import Up2k -from copyparty.util import FHC, CachedDict, Garda, Unrecv +from copyparty.util import FHC, CachedDict, Garda, Unrecv, expand_osenv_c init_E(E) @@ -195,6 +195,7 @@ class Cfg(Namespace): du_who="all", dk_salt="b" * 16, fk_salt="a" * 16, + env_expand=2, fsnt="lin", grp_all="acct", idp_gsep=re.compile("[|:;+,]"), @@ -217,6 +218,7 @@ class Cfg(Namespace): rw_edit="md", s_rd_sz=256 * 1024, s_wr_sz=256 * 1024, + shenvexp=expand_osenv_c, shr_who="auth", sort="href", srch_hits=99999, From 37e68f60a614968c0e0f29706cedeb5f20ecf816 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 21 Apr 2026 13:41:27 +0000 Subject: [PATCH 27/27] update deps --- copyparty/tcpsrv.py | 1 + scripts/deps-docker/Dockerfile | 2 +- scripts/pyinstaller/deps.sha512 | 2 +- scripts/pyinstaller/notes.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index c98b1d3b..73fc7448 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -21,6 +21,7 @@ from .util import ( VF_CAREFUL, Netdev, atomic_move, + chkcmd, get_adapters, min_ex, sunpack, diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index a2330576..cc6f5fa3 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.3.3 \ + ver_dompf=3.4.0 \ ver_mde=2.18.0 \ ver_codemirror=5.65.18 \ ver_fontawesome=5.13.0 \ diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index 98c30e32..f76f0f1f 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -31,5 +31,5 @@ a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac7 efc712162da7fb005c8869a7612d2f4983d2d073ec79e16a58e7bf1fcd01c88b1cc26656f0893c68edd2294be7c3990db2f6bd77e7e3f2613539d57994b6a033 pillow-12.1.1-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 -50dba4a63957220247be2985bd4ed6928679d9f6dc8cb7cee36394dda4e69cdee910fb39b01965d1358133855ace535eb1e08774fed7090feb8618dfc5fd2441 python-3.13.12-amd64.exe +368ea2da3e3bfe765a37c62227e84774853aaabce6954475fa45c873e5547cb5346ca03a0f6a0789af369285bb3464881fed0275a19066913d9d396d5d9b9947 python-3.13.13-amd64.exe 2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 8f6eadd4..7f7b89e6 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -42,7 +42,7 @@ fns=( pillow-12.1.1-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.12-amd64.exe + python-3.13.13-amd64.exe setuptools-80.9.0-py3-none-any.whl ) [ $w7 ] && fns+=(