Merge remote-tracking branch 'upstream/HEAD' into hovudstraum

This commit is contained in:
Til Schmitter 2026-04-25 18:30:38 +02:00
commit bde9be9974
27 changed files with 524 additions and 126 deletions

View file

@ -89,6 +89,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [other flags](#other-flags)
* [descript.ion](#description) - add a description to each file in a folder
* [dothidden](#dothidden) - cosmetically hide specific files in a folder
* [thumbnail pregen](#thumbnail-pregen) - if you want to pre-generate everything on startup
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
* [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes
@ -1360,6 +1361,7 @@ serverlog is sent to stdout by default (but logging to a file is also possible)
* [-q](https://copyparty.eu/cli/#g-q) disables logging to stdout, and may improve performance a little bit
* combine it with `-lo logfolder/cpp-%Y-%m-%d.txt` to log to a file instead
* the `%Y-%m-%d` makes it create a new logfile every day, with the date as filename
* global-option [--rlo](https://copyparty.eu/cli/#rlo-help-page) decides what happens if the filename is taken
* `-lo whatever.txt` can be used without `-q` to log to both at the same time
* by default, the logfile will have colors if the terminal does (usually the case)
* use the [textfile-viewer](https://github.com/user-attachments/assets/8a828947-2fae-4df9-bd2a-3de46f42d478) or `less -R` in a terminal to see colors correctly
@ -3127,9 +3129,10 @@ when generating hashes using `--ah-cli` for docker or systemd services, make sur
## https
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
both HTTP and HTTPS are accepted by default, but please ignore copyparty's built-in https/tls support and instead use a [reverse proxy](#reverse-proxy) to handle https/tls/ssl
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3
* reverseproxies do a better job following [best practices](https://cipherlist.eu/) meaning they are more secure, and probably also have higher performance
* also, copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3
if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup
* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options
@ -3141,6 +3144,11 @@ to install cfssl on windows:
* rename them to `cfssl.exe`, `cfssljson.exe`, `cfssl-certinfo.exe`
* put them in PATH, for example inside `c:\windows\system32`
if you really wanna give copyparty an existing TLS certificate then do one of the following:
* `--no-crt --cert server.pem` where `server.pem` is a concatenation of key + cert + chain (in that order), or...
* `--no-crt --cert server.crt --certkey server.key` where `server.key` is the key, and `server.crt` is a concatenation of cert + chain (in that order)
* file-extensions don't matter, but all files are expected to be [PEM-style](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/res/insecure.pem)
# recovering from crashes

View file

@ -38,6 +38,10 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
# more upload stuff
* combine [reloc-by-wark-xbu.py](reloc-by-wark-xbu.py) and [reloc-by-wark-xau.py](reloc-by-wark-xau.py) to rename uploads to the checksum of the file contents
# on message
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
import os
import sys
_ = r"""
rename incoming uploads according to the "wark" (the file identifier)
which is basically but not exactly a sha512 hash of the file contents
NOTE: this does NOT work with up2k uploads (dragdrop into browser);
combine this hook with reloc-by-wark-xbu.py to fix that
example usage as global config:
-e2d --xau I,c,bin/hooks/reloc-by-wark-xau.py
parameters explained,
e2d = enable up2k database (mandatory for xau hooks)
xau = execute before upload
I = import this hook for performance; do not fork / subprocess
c = "check"; reject upload if this hook crashes due to a bug
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,e2d,xau=I,c,bin/hooks/reloc-by-wark-xau.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
e2d, xau: I,c,bin/hooks/reloc-by-wark-xau.py
"""
def main(inf):
if inf.get("wark"):
# this is an up2k upload; nothing to be done, just bail
return {}
abspath = inf["ap"] # filesystem path to the uploaded file
# we don't have the wark yet so need to calculate it;
# generating a regular sha512 would of course be much easier,
# but then filenames would be different depending on how the
# file was uploaded (laaame) so let's do it the hard way
# use the standard up2k-salt which nobody ever changes:
salt = "hunter2"
# to generate the wark we'll need some functions from copyparty;
# follow the trail to the copyparty module and grab them from there:
import inspect
libpath = inspect.getfile(inf["log"])
libpath = os.path.dirname(os.path.dirname(libpath))
sys.path.insert(0, libpath)
from copyparty.up2k import up2k_hashlist_from_file, up2k_wark_from_hashlist
chunklist, st = up2k_hashlist_from_file(abspath)
wark = up2k_wark_from_hashlist(salt, st.st_size, chunklist)
# okay nice
# the rest of the code below is just copied from reloc-by-wark-xbu.py
# -------------------------------------------------------------------------
# grab the original filename from the vpath...
vdir, fn = os.path.split(inf["vp"])
# ...to retain the original file extension, if any
try:
fn, ext = fn.rsplit(".", 1)
except:
ext = ""
# use the first 16 characters; 12 bytes of entropy,
# roughly one collision for every 26 million files
fn = wark[:16]
if ext:
ext = ext.lower()
fn += "." + ext
return {"reloc": {"fn": fn}}

View file

@ -0,0 +1,69 @@
#!/usr/bin/env python3
import os
_ = r"""
rename incoming uploads according to the "wark" (the file identifier)
which is basically but not exactly a sha512 hash of the file contents
NOTE: this only works for up2k uploads (dragdrop into browser);
combine this with reloc-by-wark-xau.py to cover the other protocols
example usage as global config:
-e2d --xbu I,c,bin/hooks/reloc-by-wark-xbu.py
parameters explained,
e2d = enable up2k database (mandatory for xbu hooks)
xbu = execute before upload
I = import this hook for performance; do not fork / subprocess
c = "check"; reject upload if this hook crashes due to a bug
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,e2d,xbu=I,c,bin/hooks/reloc-by-wark-xbu.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
e2d, xbu: I,c,bin/hooks/reloc-by-wark-xbu.py
"""
def main(inf):
wark = inf.get("wark")
if not wark:
# not an up2k upload, so we don't have the hash;
# option 1: let upload proceed with original filename
return {}
# option 2: reject the upload
return {"rejectmsg": "only up2k uploads are allowed in this volume"}
# grab the original filename from the vpath...
vdir, fn = os.path.split(inf["vp"])
# ...to retain the original file extension, if any
try:
fn, ext = fn.rsplit(".", 1)
except:
ext = ""
# use the first 16 characters; 12 bytes of entropy,
# roughly one collision for every 26 million files
fn = wark[:16]
if ext:
ext = ext.lower()
fn += "." + ext
return {"reloc": {"fn": fn}}

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.19"
S_BUILD_DT = "2026-01-18"
S_VERSION = "2.20"
S_BUILD_DT = "2026-04-22"
"""
u2c.py: upload to copyparty
@ -1184,7 +1184,7 @@ class Ctl(object):
handshake(self.ar, file, False)
def cleanup_vt100(self):
if VT100:
if VT100 and not self.ar.ns:
ss.scroll_region(None)
else:
eprint("\033]9;4;0\033\\")

View file

@ -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.13"
pkgver="1.20.14"
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=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271")
sha256sums=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.13
pkgver=1.20.14
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=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271")
sha256sums=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.13/copyparty-1.20.13.tar.gz",
"version": "1.20.13",
"hash": "sha256-sq+SUPfvl6XfJt9BLuCCxtK+DwzTHVebT7tqovPlwnE="
"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="
}

View file

@ -70,7 +70,10 @@ from .util import (
expand_osenv_noop,
expand_osenv_s,
has_resource,
list_ips,
list_nics,
load_resource,
lprint,
min_ex,
pybin,
read_utf8,
@ -98,7 +101,6 @@ except:
HAVE_SSL = False
u = unicode
printed: list[str] = []
zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
@ -174,16 +176,6 @@ class BasicDodge11874(
super(BasicDodge11874, self).__init__(*args, **kwargs)
def lprint(*a: Any, **ka: Any) -> None:
eol = ka.pop("end", "\n")
txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt)
if not VT100:
txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka)
def warn(msg: str) -> None:
lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
@ -644,6 +636,9 @@ def get_sects():
\033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3
\033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs
\033[33m--list-nics\033[0m shows all network adapters (also offline ones);
\033[33m--list-ips\033[0m shows all LAN IPs
"""
),
],
@ -969,6 +964,7 @@ def get_sects():
values for --urlform:
\033[36mstash\033[35m dumps the data to file and returns length + checksum
\033[36msave,get\033[35m dumps to file and returns the page like a GET
\033[36mget\033[35m ignores the message and returns the page like a GET
\033[36mprint \033[35m prints the data to log and returns an error
\033[36mprint,xm \033[35m prints the data to log and returns --xm output
\033[36mprint,get\033[35m prints the data to log and returns GET\033[0m
@ -1029,6 +1025,23 @@ def get_sects():
"""
),
],
[
"rlo",
"logrotate format",
dedent(
"""
a logrotate-counter is added if the logfile filename is taken;
by default at the end, unless \033[32m%R\033[0m is somewhere in the \033[36m-lo\033[0m pattern,
for example: -lo /var/log/cpp/%Y-%m-%d%R.txt
\033[36m--rlo\033[0m configures the logrotate format; examples:
.1 = when necessary, append a dot followed by a single digit
.1! = counter is always added, even when not necessary
-3 = a hyphen followed by three-digit counter
(blank) = disable counter; overwrite existing logfile
"""
),
],
[
"ls",
"volume inspection",
@ -1216,7 +1229,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("--env-expand", metavar="N", type=int, default=-1, help="expand environment-variables in config-files? [\033[32m0\033[0m]=no, [\033[32m1\033[0m]=$VAR (old scary syntax), [\033[32m2\033[0m]=${VAR} (new recommended syntax); default is new-syntax with panic if old-syntax is seen")
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)")
@ -1350,13 +1363,16 @@ def add_network(ap):
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds")
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
ap2.add_argument("--list-nics", action="store_true", help="debug: list detected network adapters")
ap2.add_argument("--list-ips", action="store_true", help="debug: list detected LAN IPs")
def add_tls(ap, cert_path):
ap2 = ap.add_argument_group("SSL/TLS options")
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain")
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain (if \033[33m--certkey\033[0m is not set), or just the certificate chain (if \033[33m--certkey\033[0m is set)")
ap2.add_argument("--certkey", metavar="PATH", type=u, default="", help="path to file containing just the certificate key; if this is set, then \033[33m--cert\033[0m should only contain the certificate chain")
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
@ -1588,10 +1604,10 @@ def add_stats(ap):
def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
@ -1689,6 +1705,7 @@ def add_logging(ap):
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
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("--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")
@ -1888,7 +1905,7 @@ def add_ui(ap, retry: int):
ap2.add_argument("--grid", action="store_true", default="true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", default="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="4", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)")
ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="4", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, 6, 6c, 7, 7c, 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 / ...")
@ -2114,6 +2131,14 @@ def main(argv: Optional[list[str]] = None) -> None:
print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
sys.exit(0)
if "--list-ips" in argv:
print("\n".join(str(x) for x in sorted(list_ips())))
sys.exit(0)
if "--list-nics" in argv:
print("\n".join(str(x) for x in sorted(list_nics(True).items())))
sys.exit(0)
if EXE:
print("pybin: {}\n".format(pybin), end="")
@ -2301,7 +2326,7 @@ def main(argv: Optional[list[str]] = None) -> None:
# signal.signal(signal.SIGINT, sighandler)
SvcHub(al, dal, argv, "".join(printed)).run()
SvcHub(al, dal, argv).run()
if __name__ == "__main__":

View file

@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 13)
VERSION = (1, 20, 14)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 3, 23)
BUILD_DT = (2026, 4, 24)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View file

@ -51,9 +51,12 @@ def ensure_cert(log: "RootLogger", args) -> None:
with open(args.cert, "wb") as f:
f.write(cert_insec)
if args.certkey and not os.path.isfile(args.certkey):
raise Exception("certificate-key file does not exist: " + args.certkey)
with open(args.cert, "rb") as f:
buf = f.read()
o1 = buf.find(b" PRIVATE KEY-")
o1 = buf.find(b" PRIVATE KEY-") if not args.certkey else 0
o2 = buf.find(b" CERTIFICATE-")
m = "unsupported certificate format: "
if o1 < 0:
@ -252,7 +255,7 @@ def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
if args.http_only:
return
if args.no_crt or not HAVE_CFSSL:
if args.no_crt or args.certkey or not HAVE_CFSSL:
ensure_cert(log, args)
return

View file

@ -616,6 +616,8 @@ class Ftpd(object):
print(t.format(pybin))
sys.exit(1)
if self.args.certkey:
h1.keyfile = self.args.certkey
h1.certfile = self.args.cert
h1.tls_control_required = True
h1.tls_data_required = True

View file

@ -4897,15 +4897,8 @@ class HttpCli(object):
dl_id,
)
sent = (eof - ofs) - remains
ofs = eof - remains
f.seek(ofs)
try:
st2 = os.stat(open_args[0])
if st.st_ino == st2.st_ino:
st = st2 # for filesize
except:
pass
f.seek(eof - remains)
ofs = f.tell()
gone = 0
unsent = False
@ -4925,6 +4918,7 @@ class HttpCli(object):
t_fd = t_ka = now
self.s.sendall(buf)
sent += len(buf)
ofs += len(buf)
unsent = False
dls[dl_id] = (time.time(), sent)
continue
@ -4935,14 +4929,9 @@ class HttpCli(object):
self.s.send(b"\x00")
if t_fd < now - sec_fd:
try:
st2 = os.stat(open_args[0])
szd = st2.st_size - st.st_size
if (
st2.st_ino != st.st_ino
or st2.st_size < sent
or szd < 0
or unsent
):
st2 = os.stat(abspath)
szd = st2.st_size - ofs
if st2.st_ino != st.st_ino or szd < 0 or unsent:
assert f # !rm
# open new file before closing previous to avoid toctous (open may fail; cannot null f before)
f2 = open_nolock(*open_args)
@ -4950,15 +4939,15 @@ class HttpCli(object):
f = f2
f.seek(0, os.SEEK_END)
eof = f.tell()
if eof < sent:
ofs = sent = 0 # shrunk; send from start
if eof < ofs:
ofs = 0 # shrunk; send from start
zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
self.s.sendall(zb)
if ofs0 < 0 and eof > -ofs0:
ofs = eof + ofs0
else:
ofs = sent # just new fd? resume from same ofs
# else: probably just new fd; resume from same ofs
f.seek(ofs)
ofs = f.tell()
self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
unsent = False
gone = 0
@ -5846,6 +5835,7 @@ class HttpCli(object):
excl, target = (target.split("/", 1) + [""])[:2]
sub = self.gen_tree("/".join([top, excl]).strip("/"), target, dk)
ret["k" + quotep(excl)] = sub
dk = ""
vfs = self.asrv.vfs
dk_sz = False
@ -5885,16 +5875,16 @@ class HttpCli(object):
else:
dirs = exclude_dotfiles(dirs)
dirs = [quotep(x) for x in dirs if x != excl]
if dk_sz and fsroot:
kdirs = []
fsroot_ = os.path.join(fsroot, "")
for dn in dirs:
for dn in [x for x in dirs if x != excl]:
ap = fsroot_ + dn
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
kdirs.append(dn + "?k=" + zs)
kdirs.append(quotep(dn) + "?k=" + zs)
dirs = kdirs
else:
dirs = [quotep(x) for x in dirs if x != excl]
if vfs_virt:
for x in vfs_virt:

View file

@ -157,7 +157,7 @@ class HttpConn(object):
try:
assert ssl # type: ignore # !rm
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(self.args.cert)
ctx.load_cert_chain(self.args.cert, self.args.certkey)
if self.args.ssl_ver:
ctx.options &= ~self.args.ssl_flags_en
ctx.options |= self.args.ssl_flags_de

View file

@ -57,6 +57,7 @@ from .util import (
HAVE_PSUTIL,
HAVE_SQLITE3,
HAVE_ZMQ,
LOG,
RE_ANSI,
URL_BUG,
UTC,
@ -74,6 +75,7 @@ from .util import (
load_ipr,
load_ipu,
lock_file,
lprinted,
min_ex,
mp,
odfusion,
@ -126,7 +128,6 @@ class SvcHub(object):
args: argparse.Namespace,
dargs: argparse.Namespace,
argv: list[str],
printed: str,
) -> None:
self.args = args
self.dargs = dargs
@ -208,8 +209,24 @@ class SvcHub(object):
else:
self.log = self._log_enabled
self.lo1 = self.lo2 = ""
if args.lo:
self._setup_logfile(printed)
if "%" in args.lo and "%R" not in args.lo:
args.lo += "%R"
if not args.rlo:
args.lo = args.lo.replace("%R", "")
try:
self.lo1, self.lo2 = args.lo.split("%R")
except:
self.lo1 = args.lo
try:
self.rot_fmt = "%%s%s%%0%sd%s" % (args.rlo[:1], args.rlo[1:2], self.lo2)
except:
self.rot_fmt = "%s.%d"
self._setup_logfile()
LOG[0] = self.log
lprinted[:] = []
lg = logging.getLogger()
lh = HLog(self.log)
@ -1229,6 +1246,9 @@ class SvcHub(object):
for x in [x.split(" ") for x in al.sftp_key or []]
}
if not al.certkey:
al.certkey = None
mte = ODict.fromkeys(DEF_MTE.split(","), True)
al.mte = odfusion(mte, al.mte)
@ -1353,7 +1373,7 @@ class SvcHub(object):
def _logname(self) -> str:
dt = datetime.now(self.tz)
fn = str(self.args.lo)
fn = str(self.lo1)
for fs in "YmdHMS":
fs = "%" + fs
if fs in fn:
@ -1361,16 +1381,18 @@ class SvcHub(object):
return fn
def _setup_logfile(self, printed: str) -> None:
base_fn = fn = sel_fn = self._logname()
do_xz = fn.lower().endswith(".xz")
if fn != self.args.lo:
ctr = 0
def _setup_logfile(self) -> None:
base_fn = fn = self._logname()
sel_fn = fn + self.lo2
do_xz = sel_fn.lower().endswith(".xz")
if "%R" in self.args.lo:
# yup this is a race; if started sufficiently concurrently, two
# copyparties can grab the same logfile (considered and ignored)
while os.path.exists(sel_fn):
ctr += 1
sel_fn = "{}.{}".format(fn, ctr)
for n in range(9999):
if n or "!" in self.args.rlo:
sel_fn = self.rot_fmt % (fn, n)
if not os.path.exists(sel_fn):
break
fn = sel_fn
try:
@ -1401,7 +1423,7 @@ class SvcHub(object):
argv = ['"{}"'.format(x) for x in argv]
msg = "[+] opened logfile [{}]\n".format(fn)
printed += msg
printed = "".join(lprinted) + msg
t = "t0: {:.3f}\nargv: {}\n\n{}"
lh.write(t.format(self.E.t0, " ".join(argv), printed))
self.logf = lh
@ -1673,7 +1695,7 @@ class SvcHub(object):
def _set_next_day(self, dt: datetime) -> None:
if self.cday and self.logf and self.logf_base_fn != self._logname():
self.logf.close()
self._setup_logfile("")
self._setup_logfile()
self.cday = dt.day
self.cmon = dt.month

View file

@ -23,6 +23,7 @@ from .util import (
atomic_move,
chkcmd,
get_adapters,
list_nics,
min_ex,
sunpack,
termsize,
@ -461,23 +462,7 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
nics = get_adapters(True)
eps: dict[str, Netdev] = {}
for nic in nics:
for nip in nic.ips:
ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip
sip = "{}/{}".format(ipa, nip.network_prefix)
nd = Netdev(sip, nic.index or 0, nic.nice_name, "")
eps[sip] = nd
try:
idx = socket.if_nametoindex(nd.name)
if idx and idx != nd.idx:
t = "netdev idx mismatch; ifaddr={} cpython={}"
self.log("tcpsrv", t.format(nd.idx, idx), 3)
nd.idx = idx
except:
pass
eps = list_nics()
netlist = str(sorted(eps.items()))
if netlist == self.netlist and self.netdevs:
return {}

View file

@ -789,6 +789,10 @@ class ThumbSrv(object):
if not ret:
return
if "vc" not in ret and "ac" in ret:
# audio in a video trenchcoat
return self.conv_spec(abspath, tpath, fmt, vn)
ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"] or ext in self.fmt_ffi:
seek: list[bytes] = []

View file

@ -3357,7 +3357,7 @@ class Up2k(object):
job["size"],
job["addr"],
job["at"],
None,
[dwark],
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
@ -4069,7 +4069,7 @@ class Up2k(object):
sz,
ip,
at or time.time(),
None,
[dwark],
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
@ -5251,7 +5251,7 @@ class Up2k(object):
job["size"],
job["addr"],
job["t0"],
None,
[job["dwrk"]],
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
@ -5708,6 +5708,31 @@ def up2k_chunksize(filesize: int) -> int:
stepsize *= mul
def up2k_hashlist_from_file(path: str) -> tuple[list[str], os.stat_result]:
"""not used by copyparty itself, only by some hooks"""
st = bos.stat(path)
fsz = st.st_size
csz = up2k_chunksize(fsz)
ret = []
with open(fsenc(path), "rb", 256 * 1024) as f:
while fsz > 0:
hashobj = hashlib.sha512()
rem = min(csz, fsz)
fsz -= rem
while rem > 0:
buf = f.read(min(rem, 64 * 1024))
if not buf:
raise Exception("EOF at " + str(f.tell()))
hashobj.update(buf)
rem -= len(buf)
digest = hashobj.digest()[:33]
ret.append(ub64enc(digest).decode("ascii"))
return ret, st
def up2k_wark_from_hashlist(salt: str, filesize: int, hashes: list[str]) -> str:
"""server-reproducible file identifier, independent of name or location"""
values = [salt, str(filesize)] + hashes

View file

@ -62,6 +62,20 @@ def noop(*a, **ka):
pass
def lprint(*a: "Any", **ka: "Any") -> None:
eol = ka.pop("end", "\n")
txt = " ".join(unicode(x) for x in a) + eol
lprinted.append(txt)
if not VT100 and "\033" in txt:
txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka)
lprinted: list[str] = []
LOG: list["Callable[..., None]"] = [lprint]
try:
from datetime import datetime, timezone
@ -788,7 +802,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
if log:
log(t, 3)
else:
print(t)
LOG[0]("#", t)
return buf.decode("utf-8", "replace")
t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
@ -796,7 +810,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
if log:
log(t, 3)
else:
print(t)
LOG[0]("#", t)
raise NotUTF8(t)
@ -1573,12 +1587,18 @@ def _expand_osenv_c(txt) -> str:
ret = zsl[0]
for v in zsl[1:]:
if "}" not in v:
raise Exception("missing '}' after %r in config-value %r" % (v, txt))
t = "missing '}' after %r in config-value %r" % (v, txt)
LOG[0]("ERROR:", t)
raise Exception(t)
a, b = v.split("}", 1)
try:
ret += os.environ[a] + b
continue
except:
raise Exception("env-var %r not defined; config-value %r" % (a, txt))
pass
t = "env-var %r not defined; config-value %r" % (a, txt)
LOG[0]("ERROR:", t)
raise Exception(t)
return ret
@ -1595,8 +1615,24 @@ def expand_osenv_cs(txt) -> str:
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,))
t = "config-value %r is using old syntax for environment-variables; choose one of the following:\noption 1: update the config-value to the new syntax; ${VAR} instead of $VAR or %%VAR%%\noption 2: allow and expand old-syntax with global-option --env-expand 1 (risky)\noption 3: ignore/disable expansion of old-syntax with global-option --env-expand 2\noption 4: disable all env-var expansions by setting env-var PRTY_NO_ENVEXPAND=1"
t = t % (txt,)
LOG[0]("WARNING:", t)
try:
_, _ = txt.split("$")
zs = r"\$(LOGS_DIRECTORY|XDG_[A-Z]+_HOME|XDG_[A-Z]+_DIR)\b"
txt = re.sub(zs, r"${\1}", txt)
a = expand_osenv_c(txt)
b = expand_osenv_s(txt)
if a == b:
return a
except:
pass
raise Exception(t)
def rice_tid() -> str:
@ -3140,6 +3176,31 @@ def list_ips() -> list[str]:
return list(ret)
def list_nics(alll: bool = False) -> dict[str, Netdev]:
nics = get_adapters(alll)
eps: dict[str, Netdev] = {}
for nic in nics:
name = nic.nice_name
try:
idx = socket.if_nametoindex(name)
if idx and idx != nic.index:
LOG[0]("#", "nic-idx mismatch; ifaddr=%r libc=%r" % (nic.index, idx), 3)
except:
idx = nic.index
for nip in nic.ips:
ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip
sip = "%s/%s" % (ipa, nip.network_prefix)
nd = Netdev(sip, idx or 0, name, "")
eps[sip] = nd
if alll and not nic.ips:
zs = "no-ip-%s" % (idx,)
eps[zs] = Netdev(zs, idx or 0, name, "")
return eps
def build_netmap(csv: str, defer_mutex: bool = False):
csv = csv.lower().strip()
@ -4097,8 +4158,11 @@ def _runhook(
"src": src,
}
if txt:
ja["txt"] = txt[0]
ja["body"] = txt[1]
if src in ("xm", "xban"):
ja["txt"] = txt[0]
ja["body"] = txt[1]
else:
ja["wark"] = txt[0] # acshually the dwark but less confusing
if imp:
ja["log"] = log
mod = loadpy(acmd[0], False)
@ -4211,7 +4275,7 @@ def runhook(
else:
ret[k] = v
except Exception as ex:
(log or print)("hook: %r, %s" % (ex, ex))
(log or print)("hook failed; %s:\n%s" % (ex, min_ex()))
if ",c," in "," + cmd:
return {"rc": 1}
break

View file

@ -1227,16 +1227,20 @@ ebi('op_cfg').innerHTML = (
'<div>\n' +
' <h3 id="h_filesize">🔢 ' + L.cl_hfsz + '</h3>\n' +
' <div><select id="fszfmt">\n' +
' <option value="0">0 ┃ 1234567</option>\n' +
' <option value="1">1 ┃ 1 234 567</option>\n' +
' <option value="2">2- ┃ 1.18 M</option>\n' +
' <option value="2c">2c ┃ 1.18 M</option>\n' +
' <option value="3">3- ┃ 1.2 M</option>\n' +
' <option value="3c">3c ┃ 1.2 M</option>\n' +
' <option value="4">4- ┃ 1.18 MB</option>\n' +
' <option value="4c">4c ┃ 1.18 MB</option>\n' +
' <option value="5">5- ┃ 1.2 MB</option>\n' +
' <option value="5c">5c ┃ 1.2 MB</option>\n' +
' <option value="0">0 ┃ 2345678</option>\n' +
' <option value="1">1 ┃ 2 345 678</option>\n' +
' <option value="2">2- ┃ 2.24 M /1024</option>\n' +
' <option value="2c">2c ┃ 2.24 M /1024</option>\n' +
' <option value="3">3- ┃ 2.2 M /1024</option>\n' +
' <option value="3c">3c ┃ 2.2 M /1024</option>\n' +
' <option value="4">4- ┃ 2.24 Mi /1024</option>\n' +
' <option value="4c">4c ┃ 2.24 Mi /1024</option>\n' +
' <option value="5">5- ┃ 2.2 Mi /1024</option>\n' +
' <option value="5c">5c ┃ 2.2 Mi /1024</option>\n' +
' <option value="6">6- ┃ 2.35 MB /1000</option>\n' +
' <option value="6c">6c ┃ 2.35 MB /1000</option>\n' +
' <option value="7">7- ┃ 2.3 MB /1000</option>\n' +
' <option value="7c">7c ┃ 2.3 MB /1000</option>\n' +
' <option value="fuzzy">fuzzy</option>\n' +
' </select></div>\n' +
'</div>\n' +

View file

@ -282,7 +282,7 @@ function U2pvis(act, btns, uc, st) {
nb = fo.bt * (++fo.nh / fo.cb.length),
p = r.perc(nb, 0, fobj.size, fobj.t_hashing);
fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s';
fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MiB/s';
if (!r.is_act(fo.in))
return;
@ -302,7 +302,7 @@ function U2pvis(act, btns, uc, st) {
return;
var p = r.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading);
fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s';
fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MiB/s';
if (!r.is_act(fo.in))
return;
@ -1664,7 +1664,7 @@ function up2k_init(subtle) {
}
if (!nhash) {
var h = L.u_etadone.format(humansize(st.bytes.hashed), pvis.ctr.ok + pvis.ctr.ng);
var h = L.u_etadone.format(humansize(st.bytes.hashed, 2), pvis.ctr.ok + pvis.ctr.ng);
if (st.eta.h !== h) {
st.eta.h = ebi('u2etah').innerHTML = h;
console.log('{0} hash, {1} up, {2} busy'.format(
@ -1675,7 +1675,7 @@ function up2k_init(subtle) {
}
if (!nsend && !nhash) {
var h = L.u_etadone.format(humansize(st.bytes.uploaded), pvis.ctr.ok + pvis.ctr.ng);
var h = L.u_etadone.format(humansize(st.bytes.uploaded, 2), pvis.ctr.ok + pvis.ctr.ng);
if (st.eta.u !== h)
st.eta.u = ebi('u2etau').innerHTML = h;
@ -1740,7 +1740,7 @@ function up2k_init(subtle) {
donut.eta = eta;
st.eta[eid] = '{0}, {1}/s, {2}'.format(
humansize(rem), humansize(bps, 1), humantime(eta));
humansize(rem, 2), humansize(bps, 1), humantime(eta));
if (!etaskip)
ebi(hid).innerHTML = st.eta[eid];
@ -2721,7 +2721,7 @@ function up2k_init(subtle) {
var spd1 = (t.size / ((t.t_hashed - t.t_hashing) / 1000.)) / (1024 * 1024.),
spd2 = (t.size / ((t.t_uploaded - t.t_uploading) / 1000.)) / (1024 * 1024.);
pvis.seth(t.n, 2, 'hash {0}, up {1} MB/s'.format(
pvis.seth(t.n, 2, 'hash {0}, up {1} MiB/s'.format(
f2f(spd1, 2), !isNum(spd2) ? '--' : f2f(spd2, 2)));
pvis.move(t.n, 'ok');

View file

@ -979,17 +979,24 @@ function f2f(val, nd) {
}
var HSZ_U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
function humansize(b, terse) {
var HSZ_U = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
var HSZ_U2 = ['B', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi'];
var HSZ_UD = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
function humansize(b, tersity) {
var i = 0;
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) +
' ' + (terse ? HSZ_U[i].charAt(0) : HSZ_U[i]));
' ' + (tersity ? HSZ_U[i].slice(0, tersity) : HSZ_U[i]));
}
function humansize_su(b) {
var i = 0;
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
return [b, HSZ_U[i]];
return [b, HSZ_U2[i]];
}
function humansize_sud(b) {
var i = 0;
while (b >= 1000 && i < 5) { b /= 1000; i += 1; }
return [b, HSZ_UD[i]];
}
function humansize_0(b) {
return '' + b;
@ -1013,6 +1020,14 @@ function humansize_5g(b) {
var z = humansize_su(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];
}
function humansize_6g(b) {
var z = humansize_sud(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 100 ? 0 : b >= 10 ? 1 : 2)) + ' ' + u, u.charAt(0)];
}
function humansize_7g(b) {
var z = humansize_sud(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];
}
function humansize_2(b) {
return humansize_2g(b)[0];
}
@ -1025,6 +1040,12 @@ function humansize_4(b) {
function humansize_5(b) {
return humansize_5g(b)[0];
}
function humansize_6(b) {
return humansize_6g(b)[0];
}
function humansize_7(b) {
return humansize_7g(b)[0];
}
function humansize_2c(b) {
var v = humansize_2g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
@ -1041,6 +1062,14 @@ function humansize_5c(b) {
var v = humansize_5g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_6c(b) {
var v = humansize_6g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_7c(b) {
var v = humansize_7g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_fuzzy(b) {
if (b <= 0) return "yes";
if (b <= 80) return "hullkort";
@ -1063,7 +1092,7 @@ function humansize_fuzzy(b) {
if (b <= 50050000000) return "BD-DL";
return "LTO";
}
var humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', 'fuzzy'];
var humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', '6', '6c', '7', '7c', 'fuzzy'];
window.filesizefun = (function () {
var v = sread('fszfmt', humansize_fmts);
return window['humansize_' + (v || window.dfszf)] || humansize_1;

View file

@ -1,3 +1,74 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0323-0328 `v1.20.13` dothidden
## 🧪 new features
* #1351 add [.hidden](https://github.com/9001/copyparty/#dothidden) support (thx @NecRaul!) beb634dc 134e378e
* cosmetic filter to exclude specific files from directory listings by adding their filenames to a textfile named `.hidden` similar to many linux desktop file managers
* the files are still easily available from various APIs; this is **not** a security feature, just a way to keep things neat and tidy
* #1381 thumbnail pregeneration 7d6b037d
* usually/generally not a good idea; [readme explains it](https://github.com/9001/copyparty/#thumbnail-pregen)
* shares: now possible to grant the `.` permission to see dotfiles 66f9c950
## 🩹 bugfixes
* #1372 #1333 no thumbnails if the server OS was too old to have JXL support and the webbrowser was asking for JXL 1afe48b8
* #1363 new-version alert would only appear if the visitor had the Admin permission in the webroot specifically; now `A` in any volume is sufficient 6eb4f0ad
* 66f1ef63 should have blocked mkdir too and now it does (thx @restriction!) ac60a1da
* setting the `nohtml` or `noscript` volflags on the webroot would break the web-UI eb028c92
* shares: the [-ed](https://copyparty.eu/cli/#g-ed) global-option did not make dotfiles visible in shares 66f9c950
* the `dots` volflag still doesn't, but that one is intentional
## 🔧 other changes
* tried to stop libvips from gobbling up ram while creating jxl thumbnails; didn't really work abdbd69a
* jxl support in libvips is now default-disabled unless the libc is musl and the allocator is mallocng, which means alpine linux
* in other words, libvips is still fully enabled in the `iv` and `dj` docker images if you do not enable mimalloc
* all other deployments will now have slightly slower jxl thumbnail generation by using ffmpeg instead (it's fine really)
* new global-option [--th-vips-jxl](https://copyparty.eu/cli/#g-th-vips-jxl) lets you force-enable it if you dare
* volflags `nohtml` and `noscript` now available as global-options `--no-html` and `--no-script` 5f3b76c8
* and the `-ss` paranoia option now also enables `--no-html --no-readme --no-logues`
* [--flo 2](https://copyparty.eu/cli/#g-flo) now removes colors from logfiles even if [-q](https://copyparty.eu/cli/#g-q) is not set 8c6d8a3c
* update dompurify to 3.3.3 6a9e6da8
* docs:
* #1360 versus.md: more readable headers (thx @eugenesvk!) e71e1900
* #1367 mention [--shr-who](https://copyparty.eu/cli/#g-shr-who) in the readme (thx @TWhiteShadow!) 4688410f
## 🌠 fun facts
* it is easter soon edc20175
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0311-0042 `v1.20.12` fix shares in ftp/sftp
## ⚠️ ATTN: this release fixes an ftp/sftp issue with shares
* [GHSA-67rw-2x62-mqqm](https://github.com/9001/copyparty/security/advisories/GHSA-67rw-2x62-mqqm): when a share is created for just one or more files inside a folder, it was possible to use FTP or SFTP to access the other files inside that folder by guessing the filenames
* so ignore this issue if you did not enable [ftp](https://copyparty.eu/cli/#g-ftp) or [sftp](https://copyparty.eu/cli/#g-sftp) in the server config
* it was not possible to descend into subdirectories in this manner; only the sibling files were accessible
* NOTE: this does NOT affect filekeys; this is specifically regarding the [shr](https://copyparty.eu/cli/#g-shr) global-option
* password-protected shares were not affected through SFTP, only FTP
this release also fixes [GHSA-rcp6-88mm-9vgf](https://github.com/9001/copyparty/security/advisories/GHSA-rcp6-88mm-9vgf) but that one is nothing to worry about
## 🧪 new features
* features? in this econonmy?? ain't nobody got time for that
## 🩹 bugfixes
* 66f1ef63547a8c5f45dc2472801d2a973ff997cc [GHSA-67rw-2x62-mqqm](https://github.com/9001/copyparty/security/advisories/GHSA-67rw-2x62-mqqm) (shares)
* 9f9d30f42c89d1d5fc79ae745f136a9d5f857192 [GHSA-rcp6-88mm-9vgf](https://github.com/9001/copyparty/security/advisories/GHSA-rcp6-88mm-9vgf) (the other thing)
## 🌠 fun facts
* the [first cve](https://github.com/9001/copyparty/security/advisories/GHSA-pxfv-7rr3-2qjg) is still by far the worst, none of the others even close... so at least that's nice
* if you saw the cve notification and got all worked up, here's some [comfy music to relax and upgrade copyparty to](https://www.youtube.com/watch?v=A4zlH2mzMHw&list=PLRKwPvvniAjauumQljdrWAImRQGF3mCRU&index=1)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0308-2106 `v1.20.11` what? nohtml is evolving!

View file

@ -264,6 +264,7 @@ sz=3321225472; csz=16777216;
sz=4394967296; csz=25165824;
sz=6509559808; csz=33554432;
sz=138438953472; csz=50331648;
sz=85932900352; csz=$((1024*1024*4)); # flippy bd
f=csz-$csz; truncate -s $sz $f; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=$f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done
# py2 on osx

View file

@ -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.0 \
ver_dompf=3.4.1 \
ver_mde=2.18.0 \
ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \

View file

@ -21,6 +21,6 @@ echo zlib=$zlib ff=$ff
[ "$1" ] && exit
[ $zlib ] && { make zlib; cp -pv 1 2 ../cver/; }
[ $ff ] && { make ff; cp -pv 3 ../cver/; }
[ $zlib ] && { make -C.. zlib; cp -pv 1 2 ../cver/; }
[ $ff ] && { make -C.. ff; cp -pv 3 ../cver/; }
rm -rf cver2

View file

@ -27,7 +27,7 @@ cat $f | awk '
sub(/\[/,"");
sub(/\]\([^)]+\)/,"");
bab=$0;
gsub(/ /,"-",bab);
gsub(/[ :]+/,"-",bab);
gsub(/\./,"",bab);
h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);
next