Compare commits

...
11 Commits
Author SHA1 Message Date
Bart Oostveen 1af7efeca8 fix(nix): separate makeWrapperArgs properly (#1676)
* fix(nix): separate makeWrapperArgs properly

Since Python packages are now built with `__structuredAttrs = true;` in nixpkgs, this is neccesary.

See also: https://github.com/NixOS/nixpkgs/commit/a8383b633210925071f3d7c680c8d901939c7fd5
See also: https://github.com/NixOS/nixpkgs/pull/554025

* chore(nix): nixfmt
2026-10-21 08:09:00 +03:00
ed 8b4593c9a2 update pkgs to 1.20.26 2026-10-14 23:30:13 +00:00
ed 7ccebb6948 v1.20.26 2026-10-14 23:21:55 +00:00
ed ed0fcd608f th: fix .raw if not pil jxl 2026-10-14 21:55:12 +00:00
ed 9811cd3101 thx faelnor 2026-10-14 21:54:58 +00:00
ed fa8913c712 dumb 2026-10-14 21:53:36 +00:00
ed 4bd64d7456 shadowing: less false-positives 2026-10-14 21:51:52 +00:00
ed 7ce483e970 xvol: ls filter 2026-10-14 21:50:40 +00:00
ed 82eab3d2f8 xvol/xdev (GHSA-hg99-jqfp-rf76) 2026-10-14 21:49:04 +00:00
ed 0dc1cb3291 docker: drop openjpeg 2026-10-09 00:26:10 +00:00
ed 1a87dc3218 update pkgs to 1.20.25 2026-10-06 00:44:54 +00:00
20 changed files with 352 additions and 125 deletions

No files matched your search

+7 -2
View File
@@ -1805,12 +1805,17 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access
* usually keeps permissions from outer/initial volume, but don't take that for granted
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
these options will reduce performance; unlikely worst-case estimates are 27% reduction for directory listings, 35% for download-as-tar
* if the underlying storage is [a network disk](#using-the-cloud-as-storage) then xvol/xdev will be **very** slow (not to mention pointless/useless)
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
> **WARNING:** the `xvol` and `xdev` options should NOT be relied on for security; these are originally and primarily intended for the [e2ds indexer](#file-indexing). Certain underlying filesystems (especially FUSE-based and casefolding ones) will be problematic, and symlinks inherently have various TOCTOUs, and there's probably also bugs in copyparty where xvol/xdev is not fully enforced for certain APIs. The developer does not care about these options and they do not receive much testing.
### periodic rescan
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
+2 -2
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.24"
pkgver="1.20.26"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -26,7 +26,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/copyparty.conf")
install=copyparty.install
sha256sums=("450dca7d5701e67235d916e035fcc2143328ce1d0a6b5b7e3c5d2c23bdbb73e3")
sha256sums=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
+2 -2
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.24
pkgver=1.20.26
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=("450dca7d5701e67235d916e035fcc2143328ce1d0a6b5b7e3c5d2c23bdbb73e3")
sha256sums=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
+36 -27
View File
@@ -91,19 +91,20 @@ let
day = builtins.substring 6 2 lastModifiedDate;
};
# ex: "1970" "1" "1"
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
dateStringsShort = builtins.mapAttrs (
_: val: toString (lib.toIntBase10 val)
) dateStringsZeroPrefixed;
unstableVersion =
if copypartyFlake == null then
"${pinData.version}-unstable"
else
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
;
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}";
version = if stable then pinData.version else unstableVersion;
stableSrc = fetchurl {
inherit (pinData) url hash;
};
root = ../../../..;
unstableSrc = nix-gitignore.gitignoreSource [] root;
unstableSrc = nix-gitignore.gitignoreSource [ ] root;
src = if stable then stableSrc else unstableSrc;
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
@@ -125,30 +126,38 @@ buildPythonApplication {
old_src_folder="''${folders[0]}"
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
'')}
${lib.optionalString (copypartyFlake != null) (
with dateStringsShort;
''
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
''
)}
'';
dependencies =
[
jinja2
fusepy
]
++ lib.optional withSMB impacket
++ lib.optional withSFTP paramiko
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips
++ lib.optional withMediaProcessing ffmpeg
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
++ lib.optional withMagic magic
++ (extraPythonPackages python.pkgs);
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
dependencies = [
jinja2
fusepy
]
++ lib.optional withSMB impacket
++ lib.optional withSFTP paramiko
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips
++ lib.optional withMediaProcessing ffmpeg
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
++ lib.optional withMagic magic
++ (extraPythonPackages python.pkgs);
makeWrapperArgs = [
"--prefix"
"PATH"
":"
(lib.makeBinPath runtimeDeps)
];
pyproject = true;
build-system = [
+3 -3
View File
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.24/copyparty-1.20.24.tar.gz",
"version": "1.20.24",
"hash": "sha256-RQ3KfVcB5nI12RbgNfzCFDMozh0Ka1t+PF0sI727c+M="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.26/copyparty-1.20.26.tar.gz",
"version": "1.20.26",
"hash": "sha256-rH6gW96NQxsyzC/2GPJ1Tka9lnvxDeqmZU2smkSab/k="
}
+2 -2
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 25)
VERSION = (1, 20, 26)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 10, 6)
BUILD_DT = (2026, 10, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+94 -14
View File
@@ -470,6 +470,7 @@ class VFS(object):
self.canonical = self._canonical_null
self.dcanonical = self._dcanonical_null
self.get = self._get
self.get_dbv = self._get_dbv
self.ls = self._ls
@@ -603,7 +604,7 @@ class VFS(object):
ret += "A"
return ret
def get(
def _get(
self,
vpath: str,
uname: str,
@@ -645,6 +646,42 @@ class VFS(object):
return vn, rem
def _get_xvd(
self,
vpath: str,
uname: str,
will_read: bool,
will_write: bool,
will_move: bool = False,
will_del: bool = False,
will_get: bool = False,
err: int = 403,
) -> tuple["VFS", str]:
zvr = self._get(
vpath, uname, will_read, will_write, will_move, will_del, will_get, err
)
if not zvr:
return None
vn, rem = zvr
if "xvd" in vn.flags and vn.realpath:
avn = vn.chk_ap(vn.canonical(rem))
if avn and avn.vpath == vn.vpath:
return vn, rem
if (
not avn
or (will_read and uname not in avn.axs.uread)
or (will_write and uname not in avn.axs.uwrite)
or (will_move and uname not in avn.axs.umove)
or (will_del and uname not in avn.axs.udel)
or (will_get and uname not in avn.axs.uget)
):
if not err:
return None
raise Pebkac(
err, "permission denied for [/%s]" % (vjoin(vn.vpath, rem))
)
return vn, rem
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
src = self.shr_src
if not src:
@@ -770,6 +807,32 @@ class VFS(object):
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
return abspath, real, {}
def _ls_xvd(
self,
rem: str,
uname: str,
scandir: bool,
permsets: list[list[bool]],
lstat: bool = False,
throw: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
dirpath, real0, virt_vis = self._ls(rem, uname, scandir, permsets, True, throw)
lns = [x for x in real0 if stat.S_ISLNK(x[1].st_mode)]
if not lns:
return dirpath, real0, virt_vis
real = [x for x in real0 if not stat.S_ISLNK(x[1].st_mode)]
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
for fn, _ in lns:
ap = absreal(os.path.join(dirpath, fn))
vn = self.chk_ap(ap)
try:
if vn.get("", uname, *ps):
real.append((fn, bos.stat(ap)))
except:
pass
real.sort()
return dirpath, real, virt_vis
def _ls(
self,
rem: str,
@@ -870,12 +933,16 @@ class VFS(object):
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
return
if "xdev" in self.flags or "xvol" in self.flags:
if "xvd" in self.flags:
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
rm1 = []
for le in vfs_ls:
ap = absreal(os.path.join(fsroot, le[0]))
vn2 = self.chk_ap(ap)
if not vn2 or not vn2.get("", uname, True, False, err=0):
try:
if not vn2.get("", uname, *ps):
raise Exception()
except:
rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
@@ -2979,6 +3046,14 @@ class AuthSrv(object):
excl.extend([r"^\.", r"/\."])
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
for vol in list(vfs.all_nodes.values()):
if "xvd" in vol.flags:
for vol in vfs.all_nodes.values():
vol.get = vol._get_xvd
if vol.ls.__name__ == "_ls":
vol.ls = vol._ls_xvd
break
have_daw = False
for vol in vfs.all_nodes.values():
daw = vol.flags.get("daw") or self.args.daw
@@ -3338,19 +3413,24 @@ class AuthSrv(object):
while nodes:
vn = nodes.pop()
nodes += list(vn.nodes.values())
if not vn.realpath:
if (
not vn.realpath
or "bcasechk" not in vn.flags
or (not vn.axs.uread and not vn.axs.uwrite and not vn.axs.uget)
):
continue
if "bcasechk" in vn.flags:
if ".hist" in vn.nodes and "e2d" in vn.flags:
zb1 = True
if vn.nodes and (len(vn.nodes) > 1 or ".hist" not in vn.nodes):
if ".hist" in vn.nodes and "e2d" in vn.flags:
zb1 = True
for zs, vnn in vn.nodes.items():
if zs != ".hist" and (vnn.realpath or "").startswith(vn.realpath):
t = "nested volume on casefolding filesystem: [/%s] inside [/%s]"
self.log(t % (vnn.vpath, vn.vpath), 3)
zb2 = True
if PY2:
continue
vn.lnodes = {
k.casefold().encode("ascii", "ignore"): v
for k, v in vn.nodes.items()
}
if PY2:
continue
vn.lnodes = {
k.casefold().encode("ascii", "ignore"): v for k, v in vn.nodes.items()
}
if zb2 and PY2:
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
self.log(t, 1)
+6 -13
View File
@@ -199,18 +199,7 @@ class FtpFs(AbstractedFS):
t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath))
if "xdev" in vfs.flags or "xvol" in vfs.flags:
ap = vfs.canonical(rem)
avfs = vfs.chk_ap(ap)
t = "Permission denied in [{}]"
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
else:
ap = vfs.canonical(rem, False)
ap = vfs.canonical(rem, False)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
@@ -320,7 +309,11 @@ class FtpFs(AbstractedFS):
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
try:
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
except Exception as ex:
raise FSE(str(ex))
if not vfs.realpath:
self.cwd = nwd
return
+18 -5
View File
@@ -885,10 +885,9 @@ class HttpCli(object):
uparam["b"] = ""
cookies["b"] = ""
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
vn, rem = self.asrv.vfs._get(self.vpath, self.uname, False, False)
if vn.realpath and "xvd" in vn.flags:
avn = vn.chk_ap(vn.canonical(rem))
else:
avn = vn
@@ -2475,6 +2474,9 @@ class HttpCli(object):
if "%" in self.req:
self.log(" `-- %r" % (self.vpath,))
if not self.avn:
raise Pebkac(403, "no access")
if self.headers.get("expect", "").lower() == "100-continue":
try:
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
@@ -3830,6 +3832,8 @@ class HttpCli(object):
fdir = vfs.canonical(rem)
fn = os.path.join(fdir, sanitized)
self.asrv.vfs.get(vjoins(vfs.vpath, rem, sanitized), self.uname, False, True)
for hn in ("xbu", "xau"):
xxu = vfs.flags.get(hn)
if xxu:
@@ -3987,6 +3991,13 @@ class HttpCli(object):
if rnd:
fname = rand_name(fdir, fname, rnd)
self.can_delete = (
self.uname
in self.asrv.vfs.get(
vjoin(upload_vpath, fname), self.uname, False, True
)[0].axs.udel
)
abspath = os.path.join(fdir, fname)
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
@@ -4344,7 +4355,7 @@ class HttpCli(object):
bos.makedirs(fp, vf=vfs.flags)
fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/")
rem = vjoin(rp, fn)
dbv, vrem = vfs.get_dbv(rem)
if not self.can_delete and (
@@ -4362,6 +4373,8 @@ class HttpCli(object):
self.reply(response.encode("utf-8"))
return True
self.asrv.vfs.get(vjoin(vfs.vpath, rem), self.uname, True, True)
srv_lastmod = -1.0
srv_lastmod3 = -1
try:
+1 -12
View File
@@ -378,18 +378,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
# return "", vn, rem
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
if "xdev" in vn.flags or "xvol" in vn.flags:
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
t = "Permission denied in [{}]"
if not avn:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
else:
ap = vn.canonical(rem, False)
ap = vn.canonical(rem, False)
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
-5
View File
@@ -276,11 +276,6 @@ class Tftpd(object):
raise Exception("unmapped vfs")
ap = vfs.canonical(rem)
if "xdev" in vfs.flags or "xvol" in vfs.flags:
avn = vfs.chk_ap(ap)
for n, zb in enumerate(perms):
if not avn or zb and not avn.uaxs["*"][n]:
raise Exception("no access")
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise Exception("file not found")
+3 -1
View File
@@ -102,7 +102,9 @@ class ThumbCli(object):
):
sfmt = "j"
if sfmt == "x" and not self.can_jxl:
if sfmt == "x" and (
not self.can_jxl or (not H_PIL_JXL and ext in self.fmt_raw)
):
sfmt = "w"
vf_crop = dbv.flags["crop"]
+2 -2
View File
@@ -1179,7 +1179,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set zipmax zipmaxn_v zipmaxs_v"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set xvd zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@@ -1317,7 +1317,7 @@ class Up2k(object):
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
):
try:
if self.fstab.get(ptop)[0] != "btrfs":
if self.fstab.get(histpath)[0] != "btrfs":
raise Exception()
runcmd([b"chattr", b"-c", fsenc(histpath)], 1) # compression and...
runcmd([b"chattr", b"+C", fsenc(histpath)], 1) # no-cow are incompat
+2
View File
@@ -469,6 +469,8 @@ IMPLICATIONS = [
["smbvvv", "smbvv"],
["smbvv", "smbv"],
["smbv", "smb"],
["xdev", "xvd"],
["xvol", "xvd"],
["zv", "zmv"],
["zv", "zsv"],
["z", "zm"],
+1 -1
View File
@@ -4096,7 +4096,7 @@ var fileman = (function () {
if (wr)
html.push('<a href="#" class="btn">write-only</a>');
html.push('</td></tr></div');
html.push('</td></tr><table></div>');
shui.innerHTML = html.join('\n');
var sh_rand = ebi('sh_rand'),
+74
View File
@@ -1,3 +1,77 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-1006-0035 `v1.20.25` SECURITY: shadowing on windows/mac
## ⚠️ ATTN: this release mitigates a Windows/MacOS-specific issue with shadowing
[GHSA-3c88-p299-7hvr](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr): due to windows/macos having case-insensitive (casefolding) filesystems, [shadowing](https://github.com/9001/copyparty/#shadowing) is not bulletproof, even with this fix
## 🧪 new features
* #1498 [--acao-re](https://copyparty.eu/cli/#g-acao-re) for allowing cross-domain access with more complex rules ab2299e3
* #1652 [redup](https://copyparty.eu/cli/#redup-help-page): detect and skip hardlinks / reflinks which are already optimally deduplicated 08093578
* [--sftp-fastldr](https://copyparty.eu/cli/#g-sftp-fastldr) to improve startup time on extremely old hardware 186cbf93
* [--sftp-chattr](https://copyparty.eu/cli/#g-sftp-chattr) to restrict chattr to certain properties e59fc05e
## 🩹 bugfixes
* GHSA-3c88-p299-7hvr e8de4211
* for further context, torvalds has some [technically accurate and colourful opinions](https://lore.kernel.org/lkml/CAHk-=wjajMJyoTv2KZdpVRoPn0LFZ94Loci37WLVXmMxDbLOjg@mail.gmail.com/) (nsfw) on how casefolding filesystems are an ongoing mistake
* on a more serious note, [the readme was updated](https://github.com/9001/copyparty/#shadowing)
* fix uploads struggling to resume from a network glitch if the server filesystem is FAT32 or exFAT 23e00c0a 731dbbff
* #1647 shares: fix cleanup of single-file shares when manually deleted before expiration (thx @willie6666!) f445bfa8
* #1660 ban-pw now more aggressive on failed basic-auth (thx @PAStheLoD!) 5e0a88e3
* #1656 webdav: return 404 instead of 400 when file didn't exist 74b17374
* fix navigating to an index.html webroot (thx @tilse!) 334dccc5
* fix IP-address detection on Windows XP 107dec9e
## 🔧 other changes
* #1658 constant-time verification of filekeys/dirkeys (thx @rozbb!) a31870cb
* prevent enabling the smb server unless `--hack-me-bro` is also enabled because the smb-server was intentionally made super-insecure so it would work with ancient outdated equipment in emergencies and should never be used otherwise 1341b342
* now possible to upload [09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac](https://www.youtube.com/watch?v=btPU_rluFSE) (dont click that link on acid) 28f9f6f0 64f574fe
## 🌠 fun facts
* [thx t+paz](https://a.ocv.me/pub/g/2026/10/Screenshot_20261002-221751_1.png?cache)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0919-2133 `v1.20.24` redup
## 🧪 new features
* new option [--redup](https://copyparty.eu/cli/#g-redup) (global or volflag) to convert local files between various dedup approaches fd4f4341 22915902 16a3f5d5
* turn full copies into reflinks/symlinks/hardlinks or convert between those
* run it with `--redup-dry` (dryrun) to check for unexpected consequences
* WARNING: maybe take a snapshot of your filesystem before enabling this; brand new code with little testing, read [--help-redup](https://copyparty.eu/cli/#redup-help-page) and use with care
* seems fine for converting between symlinks and reflinks, probably hardlinks too
* #1643 textfile-viewer can detect filetype from shebang (thx @botahamec!) 097b8305
* when dedup with [reflink](https://copyparty.eu/cli/#g-reflink) is enabled, then also dedup between volumes when possible (since reflink makes that safe) 735d6e00 5ddbce31
* option [-i no](https://copyparty.eu/cli/#bind-help-page) to launch copyparty without the webserver, for indexing / testing / whatever c92435c2
## 🩹 bugfixes
* if `--dedup` was enabled, then copying a file from one volume to another could dedup (create a symlink/hardlink/reflink) instead of a full copy 947d73ee
* mainly an issue for portable storage such as flashdrives, and the main reason why `--redup` was added
* the list of filenames given as input to xiu hooks did not have a trailing newline c688bb2e
* #1611 volflag `nosub` would still result in empty folders from up2k ec4a95e1
* fix `PRTY_NO_IFADDR` 6867ccc5
## 🔧 other changes
* iOS: further improved upload-speeds on buggy iOS versions 971a62cc
* looks like apple is working on fixing this bug; iOS v27 made https way more stable but http is still just as broken, so improved the workaround a bit more
* added [--help-dedup](https://copyparty.eu/cli/#dedup-help-page) and [--help-redup](https://copyparty.eu/cli/#redup-help-page) 22915902 16a3f5d5
* add a warning that the sourceforge project is not official and probably malware bfc3cff8
## 🌠 fun facts
* the [railgun roulette vip-mix](https://www.youtube.com/watch?v=mv9BmrzkiJc) has the same bpm as the [turn-signal on a honda jazz](https://a.ocv.me/pub/stuff/music/railgun-roulette-honda-remix.opus)
* on that note, [this fantasy-OPNA remake](https://www.youtube.com/watch?v=hv2hr8aPBMU) is too awesome to leave out
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0906-2249 `v1.20.23` rcm once again
+1 -1
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.15 \
ver_dompf=3.4.17 \
ver_mde=2.18.0 \
ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \
+8
View File
@@ -38,6 +38,14 @@ f=/z/whl/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl
}
rm -rf /z/whl
# drop openjpeg; unmaintained, has vulns
(cd /usr/lib/python3.*/site-packages/PIL
sed -ri /jpg_2000/d features.py
sed -ri /Jpeg2KIm/d __init__.py
sed -ri /Jpeg2KIm/d Image.py
rm -f Jpeg2KImagePlugin.py
) || true
# cleanup for flavors with python build steps (dj/iv)
rm -rf /var/cache/apk/* /root/.cache
+87 -31
View File
@@ -42,7 +42,9 @@ class TestShr(unittest.TestCase):
if edl is None:
edl = els[1]
can_dl = []
for fn in b[1]:
zs = "f1 f2 f3 f4 l1 l2 l3 l4"
known_fns = list(sorted(list(set(b[1] + zs.split()))))
for fn in known_fns:
if fn == "a.db":
continue
furl = url + "/" + fn
@@ -51,7 +53,9 @@ class TestShr(unittest.TestCase):
h, zb = self.curl(furl, True)
if h.startswith("HTTP/1.1 200 "):
can_dl.append(fn)
self.assertEqual(edl, can_dl)
elif zb.startswith(b">>fb:"):
can_dl.append(fn)
self.assertEqual(edl, list(sorted(list(set(can_dl)))))
def setUp(self):
self.td = tu.get_ramdisk()
@@ -61,12 +65,15 @@ class TestShr(unittest.TestCase):
os.mkdir("d1")
os.mkdir("d2")
os.mkdir("d2/d3")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3"):
os.mkdir("d2/d3/d4")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3", "d2/d3/d4/f4"):
with open(zs, "wb") as f:
f.write(zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3"):
f.write(b">>fb:" + zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3", "d2/d3/d4"):
src, fn = zs.rsplit("/", 1)
os.symlink(absreal(zs), dst + "/l" + fn[-1:])
os.symlink(absreal("d2/d3"), absreal("d2/ld"))
os.symlink(absreal("d2/d3"), absreal("d2/d3/d4/ld"))
db = sqlite3.connect("a.db")
with db:
@@ -92,12 +99,14 @@ class TestShr(unittest.TestCase):
shr_v=False,
)
self.cinit()
L1234 = ["l1", "l2", "l3", "l4"]
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
jt = {
@@ -105,7 +114,7 @@ class TestShr(unittest.TestCase):
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
@@ -113,26 +122,48 @@ class TestShr(unittest.TestCase):
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d4",
"vp": ["/d2/d3/d4/"],
"pw": "",
"exp": "99",
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("shr/d2", False, [["ld"], ["f2"] + L1234], None)
self.assertLD("shr/d2/d3", False, [], None)
self.assertLD("shr/d2/d3/d4", False, [], None)
self.assertLD("shr/d4", False, [["ld"], ["f4"] + L1234], None)
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD("shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("shr/r/d1", False, [[], ["f1"] + L1234], None)
self.assertLD("shr/r/d2", False, [], None) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], None)
self.assertLD("shr/r/d2/d3/d4", False, [], None)
self.put("shr/d2/a", 201)
os.unlink("d2/a")
self.put("shr/d2/ld/a", 201)
os.unlink("d2/d3/a")
self.put("shr/d4/ld/a", 201)
os.unlink("d2/d3/a")
self.conn.shutdown()
@@ -147,12 +178,14 @@ class TestShr(unittest.TestCase):
xvol=True,
)
self.cinit()
L1234 = ["l1", "l2", "l3", "l4"]
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
jt = {
@@ -160,7 +193,7 @@ class TestShr(unittest.TestCase):
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
@@ -168,28 +201,45 @@ class TestShr(unittest.TestCase):
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d4",
"vp": ["/d2/d3/d4/"],
"pw": "",
"exp": "99",
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], ["f2", "l2"])
self.assertLD("shr/d2", False, [["ld"], ["f2"] + L1234], ["f2", "l2"])
self.assertLD("shr/d2/d3", False, [], [])
self.assertLD("shr/d2/d3/d4", False, [], None)
self.assertLD("shr/d4", False, [["ld"], ["f4"] + L1234], ["f4", "l4"])
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD(
"shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], ["f1", "l1", "l2"]
)
self.assertLD("shr/r/d1", False, [[], ["f1"] + L1234], ["f1", "l1", "l2"])
self.assertLD("shr/r/d2", False, [], []) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], [])
self.assertLD("shr/r/d2/d3/d4", False, [], None)
self.put("shr/d2/a", 201)
os.unlink("d2/a")
self.put("shr/d2/ld/a", 401)
self.put("shr/d4/ld/a", 401)
self.conn.shutdown()
@@ -227,3 +277,9 @@ class TestShr(unittest.TestCase):
print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def put(self, url: str, expect: str):
h = "PUT /%s HTTP/1.1\r\nContent-Length: 1\r\n\r\n`"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
h, r = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
self.assertEqual(expect, int(h.split(" ")[1]))
+3 -2
View File
@@ -163,7 +163,7 @@ class Cfg(Namespace):
ex = "ac_convt au_vol dl_list du_iwho mcr 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 gauto idp_cookie idp_store k304 loris md_nhist no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri tftp theme themes turbo u2ow zipmaxn zipmaxs"
ex = "ctl_re dav_port db_act forget_ip gauto idp_cookie idp_store k304 loris md_nhist no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri tftp theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chdir chmod_f chpw_db csp_dl csp_ui 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 redup 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"
@@ -172,7 +172,7 @@ class Cfg(Namespace):
ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner"
ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 p_nodav qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md"
@@ -227,6 +227,7 @@ class Cfg(Namespace):
th_crop="y",
th_size="320x256",
th_x3="n",
ua_nodav=re.compile("\n"),
u2sort="s",
u2ts="c",
ui_filesz="1",