mirror of
https://github.com/9001/copyparty.git
synced 2026-11-10 12:38:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ccebb6948 | ||
|
|
ed0fcd608f | ||
|
|
9811cd3101 | ||
|
|
fa8913c712 | ||
|
|
4bd64d7456 | ||
|
|
7ce483e970 | ||
|
|
82eab3d2f8 | ||
|
|
0dc1cb3291 | ||
|
|
1a87dc3218 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.25"
|
||||
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=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.24
|
||||
pkgver=1.20.25
|
||||
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=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -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.25/copyparty-1.20.25.tar.gz",
|
||||
"version": "1.20.25",
|
||||
"hash": "sha256-gsaN+kLgill1e7+gsdFAAyFBF1NHkG4Fa++DkE9cLHA="
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -469,6 +469,8 @@ IMPLICATIONS = [
|
||||
["smbvvv", "smbvv"],
|
||||
["smbvv", "smbv"],
|
||||
["smbv", "smb"],
|
||||
["xdev", "xvd"],
|
||||
["xvol", "xvd"],
|
||||
["zv", "zmv"],
|
||||
["zv", "zsv"],
|
||||
["z", "zm"],
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user