Compare commits

...
58 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
ed dc3adc0f4d v1.20.25 2026-10-06 00:35:29 +00:00
ed 5f00c1631c return to ?b=u; closes #1659 2026-10-06 00:33:31 +00:00
ed e59fc05e73 add --sftp-chattr 2026-10-05 23:47:51 +00:00
ed e8de421174 shadowing + casefolding (GHSA-3c88-p299-7hvr) 2026-10-05 23:46:39 +00:00
ed 36aaa24974 file-rename instant preview on open 2026-10-05 23:02:30 +00:00
ed 5e0a88e355 ban-pw basic-auth; closes #1660 2026-10-05 22:36:30 +00:00
ed 4b283b4bfd bbox: fix #1650 (el null) 2026-10-05 22:32:49 +00:00
ed 64f574fe94 u2c: allow \ in filenames 2026-10-05 22:26:18 +00:00
Michael Rosenberg a31870cbd3 dk/fk constant-time (#1658) 2026-10-05 22:01:30 +00:00
ed 74b17374c2 DELETE: say 404 on enoent; closes #1656 2026-10-03 22:10:17 +00:00
ed 670e86228c tests: support new coreutils or whatever 2026-10-03 22:08:10 +00:00
ed 1341b3421f --smb now requres --hack-me-bro 2026-10-03 22:01:49 +00:00
ed 5961228c93 deadcode 2026-10-03 21:44:14 +00:00
ed 28f9f6f002 allow \ in filenames on linux/macos;
now possible to upload 09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac
2026-10-03 21:43:47 +00:00
ed 186cbf939f sftp: add fastloader;
startup on an 800mhz armv5 exceeded the 30sec timeout
2026-10-02 03:29:12 +00:00
ed 080935788b redup: skip conclusive hard/ref; closes #1652 2026-09-30 00:05:02 +00:00
ed c6918c2ce6 tests: macos 2026-09-30 00:15:08 +02:00
ed ab2299e3db add acao-re; closes #1498 2026-09-27 19:12:01 +00:00
edandTil Schmitter 334dccc55e --ih: fix nav to webroot
Co-authored-by: Til Schmitter <til.schmitter@gmail.com>
2026-09-27 18:23:08 +00:00
ed 8d7aa4f860 cosmetic 2026-09-27 18:20:47 +00:00
ed f73b37c6fa pipe-dl progress on /?h 2026-09-27 18:11:29 +00:00
ed 731dbbff84 u2c: fix exfat's http502 allergy;
addendum to 23e00c0a04
2026-09-26 23:43:26 +00:00
ed 23e00c0a04 up2k-js: fix exfat's http502 allergy;
normally, to avoid excessive handshaking, unexpected server errors are
handled by continuing onto the next chunk and dealing with it at eof

this doesn't work on filesystems that dont support sparse files, so
discard the chunk-queue and re-handshake to keep it sequential
2026-09-26 22:12:15 +00:00
ed 107dec9ed1 ifaddr: winxp fix 2026-09-24 19:31:21 +00:00
willie6666 f445bfa862 shares: delete sf entries too (#1647)
Signed-off-by: willie6666 <89621369+willie6666@users.noreply.github.com>
2026-09-20 13:12:56 +00:00
ed aff083fb28 update pkgs to 1.20.24 2026-09-19 21:39:09 +00:00
ed 5ccc999f39 v1.20.24 2026-09-19 21:33:56 +00:00
ed 16a3f5d51a add --help-redup + failsafe 2026-09-19 20:48:28 +00:00
ed 35af7c9fef js: add msel fn 2026-09-19 19:14:06 +00:00
ed 4d1f2a9301 js: fix contrived crash (closes #1516);
view textfile, sel, delete, sel, boom
2026-09-19 19:13:41 +00:00
ed 229159027b add --help-dedup 2026-09-19 17:43:51 +00:00
ed ec4a95e1a5 up2k nosub; closes #1611 2026-09-19 17:42:39 +00:00
ed eb4f1dddce lint 2026-09-18 20:59:16 +00:00
Mica Whiteanded 097b83056e textfile-viewer: parse shebang (#1643)
detect filetype based on #!/interpreter

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-09-18 19:47:09 +00:00
ed c688bb2e46 xiu: add trailing newline 2026-09-17 22:00:24 +00:00
ed c92435c2d2 add -i no 2026-09-17 21:53:50 +00:00
ed 22afaa7586 nocow histpath iff btrfs 2026-09-17 21:46:12 +00:00
ed 5ddbce31fc xlink-reflink only same-fs;
avoid waking other disks which wont work anyways
2026-09-17 21:24:10 +00:00
ed ec8ec0f1a6 un-cargocult bools 2026-09-17 21:12:46 +00:00
ed 558f6aee44 shallowclone up2k.flags;
update flags on config-reload while keeping up2k coherent
2026-09-17 19:56:58 +00:00
ed 6867ccc511 fix PRTY_NO_IFADDR + cleanup 2026-09-16 22:38:23 +00:00
ed 947d73eeb6 check xlink on copy 2026-09-16 22:35:11 +00:00
ed 735d6e00de up2k: reflink is xlink-safe 2026-09-16 22:34:19 +00:00
ed fd4f434192 add --redup 2026-09-16 22:33:38 +00:00
ed 971a62cc89 up2k: iOS: reduce jitter;
* xhr.abort() to close/free tcp
* onfoo=null fixes done-flicker
2026-09-15 19:29:47 +00:00
ed bfc3cff848 unsanctioned: sourceforge 2026-09-08 06:01:30 +02:00
ed 8967726f61 update pkgs to 1.20.23 2026-09-06 22:53:14 +00:00
40 changed files with 1167 additions and 327 deletions

No files matched your search

+13 -4
View File
@@ -219,6 +219,8 @@ other places to download copyparty from (non-github links):
* https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz
* https://copyparty.eu/cli = online cli helptext
DO NOT download from [sourceforge](https://copyparty.eu/unsanctioned/sourceforge.txt)
### at home
@@ -570,7 +572,7 @@ if you want to grant access to all users who are logged in, the group `acct` wil
* to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
* single users can also be subtracted from a group: `@admins,-james`
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
anyone trying to bruteforce a password gets banned according to `--ban-pw` (assuming [real-ip](#real-ip) is configured correctly); default is 24h ban for 9 failed attempts in 1 hour
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
@@ -620,6 +622,8 @@ config file example:
hiding specific subfolders by mounting another volume on top of them
> **WARNING:** shadowing is NOT bulletproof when the underlying filesystem is casefolding (case-insensitive), in other words DON'T rely on this when copyparty is running on Windows and/or MacOS. This is also unsafe when combined with certain filesystems on Linux, especially FAT32/exFAT. Pay attention to the volume-listing in the startup log; `casechk:n` indicates probably-OK, but if it says `casechk:y` then DO NOT trust shadowing to fully prevent access to a file/subfolder.
for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
@@ -1801,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
@@ -3045,7 +3054,7 @@ below are some tweaks roughly ordered by usefulness:
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
* if you are running the copyparty server **on Windows or Macos:**
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* `--casechk=n` makes it much faster, but [reduces security](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr) and awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* this is the same as `casechk: n` in a config-file
+24 -12
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.23"
S_BUILD_DT = "2026-09-06"
S_VERSION = "2.25"
S_BUILD_DT = "2026-10-05"
"""
u2c.py: upload to copyparty
@@ -67,6 +67,8 @@ else:
WTF8 = "replace" if PY2 else "surrogateescape"
SEP = "{0}".format(os.sep).encode("ascii")
VT100 = platform.system() != "Windows"
@@ -249,7 +251,7 @@ class File(object):
def __init__(self, top, rel, size, lmod):
self.top = top # type: bytes
self.rel = rel.replace(b"\\", b"/") # type: bytes
self.rel = rel.replace(SEP, b"/") # type: bytes
self.size = size # type: int
self.lmod = lmod # type: float
@@ -267,6 +269,7 @@ class File(object):
self.wark = "" # type: str
self.url = "" # type: str
self.nhs = 0 # type: int
self.t_hs = 0.0 # type: float
# set by upload
self.t0_up = 0.0 # type: float
@@ -652,7 +655,7 @@ def walkdir(err, top, excl, seen):
def walkdirs(err, tops, excl):
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
sep = "{0}".format(os.sep).encode("ascii")
sep = SEP
if not VT100:
excl = excl.replace("/", r"\\")
za = []
@@ -904,6 +907,7 @@ def handshake(ar, file, search):
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
file.name = r["name"]
file.wark = r["wark"]
file.t_hs = time.time()
if ar.uon and not r["hash"]:
printlink(ar, file.url, r["name"], r.get("fk"))
@@ -1027,7 +1031,7 @@ class Ctl(object):
self.up_b = 0 # num bytes handled
self.up_br = 0 # num bytes actually transferred
self.uploader_busy = 0
self.serialized = False
self.oneconn = False
self.t0 = time.time()
self.t0_up = None
@@ -1213,14 +1217,15 @@ class Ctl(object):
def hasher(self):
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
sep = "{0}".format(os.sep).encode("ascii")
usep = os.sep
sep = SEP
prd = None
ls = {}
for top, rel, inf in self.filegen:
isdir = stat.S_ISDIR(inf.st_mode)
if self.ar.z or self.ar.drd:
rd = rel if isdir else os.path.dirname(rel)
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
srd = rd.decode("utf-8", "replace").replace(usep, "/").rstrip("/")
if srd:
srd += "/"
if prd != rd:
@@ -1230,7 +1235,7 @@ class Ctl(object):
print(" ls ~{0}".format(srd))
zt = (
self.ar.vtop,
quotep(rd.replace(b"\\", b"/")).decode("utf-8"),
quotep(rd.replace(sep, b"/")).decode("utf-8"),
)
sc, txt = web.req("GET", "%s%s?ls&lt&dots" % zt, {})
if sc >= 400:
@@ -1403,10 +1408,10 @@ class Ctl(object):
self.recheck.append(file)
with self.mutex:
if hs and not sprs and not self.serialized:
t = "server filesystem does not support sparse files; serializing uploads\n"
if hs and not sprs and not self.oneconn:
t = "server filesystem does not support sparse files; cannot parallelize uploads\n"
eprint(t)
self.serialized = True
self.oneconn = True
for _ in range(self.ar.j - 1):
self.q_upload.put(None)
if not hs:
@@ -1418,7 +1423,7 @@ class Ctl(object):
if not file.recheck:
self.up_done(file)
if hs and file.up_c:
if hs and file.up_c and not self.oneconn:
# some chunks failed
self.up_c -= len(hs)
file.up_c -= len(hs)
@@ -1476,6 +1481,7 @@ class Ctl(object):
self.q_upload.put(fsl)
def uploader(self):
drop_hs = (0.0, "")
while True:
fsl = self.q_upload.get()
if not fsl:
@@ -1494,6 +1500,9 @@ class Ctl(object):
cids = fsl.cids
self.last_file = file
if self.oneconn and drop_hs == (file.t_hs, file.wark):
continue
with self.mutex:
if not self.uploader_busy:
self.at_upr = time.time()
@@ -1519,6 +1528,9 @@ class Ctl(object):
t = "upload failed, retrying: %s #%s+%d (%s)\n"
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
file.cd = time.time() + self.ar.cd
if self.oneconn:
file.ucids = []
drop_hs = (file.t_hs, file.wark)
# handshake will fix it
with self.mutex:
+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.22"
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=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
sha256sums=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
+2 -2
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.22
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=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
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.22/copyparty-1.20.22.tar.gz",
"version": "1.20.22",
"hash": "sha256-FNWQZElDw+0U4hCTzOeeBvGuIGre7aFgMtFackH/Q0Q="
"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="
}
+88 -13
View File
@@ -687,6 +687,7 @@ def get_sects():
\033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1
\033[32m-i no\033[0m disable webserver
\033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m
@@ -847,6 +848,74 @@ def get_sects():
).rstrip()
+ build_flags_desc(),
],
[
"dedup",
"file deduplication on upload",
dedent(
"""
when just \033[36m--dedup\033[0m or volflag "\033[36mdedup\033[0m" is enabled, files will be
deduplicated by means of \033[1msymlinks,\033[0m which demand some caution:
the available dedup approaches:
* symlinks work on most filesystems, but if another software
moves either the symlink itself or the file that it points to,
the link will break and the file "disappears";
only safe if only copyparty is used to rename/move/delete
* hardlinks work on Linux filesystems and \033[1mlook\033[0m like regular files,
except if the contents of one copy of that file is edited, then
this will affect \033[1mall the dupes too!\033[0m
and they share the same last-modified-time
* reflinks only work on btrfs/zfs/xfs but is the safest/best choice;
deduplicated files behave just like regular files,
and can be modified without affecting other copies/dupes
to change approach, add one of the following in addition to "\033[36mdedup\033[0m":
\033[36m--reflink\033[0m or volflag "\033[36mreflink\033[0m"
if reflink is not possible (unsupp/cross-fs), a full copy is made
\033[36m--hardlink-only\033[0m or volflag "\033[36mhardlinkonly\033[0m"
if hardlink is not possible (unsupp/cross-fs), a full copy is made
\033[36m--hardlink\033[0m or volflag "\033[36mhardlink\033[0m"
if hardlink is not possible (unsupp/cross-fs), a symlink is made
note: dedup works best when combined with e2ds or e2dsa
to deduplicate existing files on-disk, see \033[36m--help-redup\033[0m
"""
),
],
[
"redup",
"deduplicating existing files on disk",
dedent(
"""
\033[36m--redup\033[0m can convert on-disk files between deduplication-approaches,
for example turning redundant full copies into reflinks to save space
converts \033[1mall files\033[0m in each volume! not just copyparty-created ones
example: --redup \033[32mno,sym,hard\033[0m=\033[35mref\033[0m will find all files of type
\033[32mfullcopy/symlink/hardlink\033[0m, and convert them into \033[35mreflinks\033[0m
(see \033[36m--help-dedup\033[0m for a summary on the types)
converting \033[1mfrom\033[0m symlinks/hardlinks is fairly fast and simple;
in this case, the obvious target type/approach is reflinks
(which only works on btrfs/zfs/xfs)
converting \033[1mto\033[0m symlinks or hardlinks is \033[3mpossible\033[0m, but
not-recommended due to risks explained in \033[36m--help-dedup\033[0m
(interaction with other softwares can result in data loss)
converting \033[1mfrom\033[0m 'no' (full redundant copies) is hella slow;
checksums each file to avoid corruption from bad RAM
"""
),
],
[
"thumb-ex",
"extract thumbnails from custom file formats with python scripts",
@@ -1212,9 +1281,9 @@ def get_sects():
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
global-option \033[33m--chmod-f\033[0m and volflag "\033[33mchmod_f\033[0m" specifies the unix-permission to use when creating a new file
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
similarly, \033[33m--chmod-d\033[0m and "\033[33mchmod_d\033[0m" sets the directory/folder perm
the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc.
@@ -1381,8 +1450,8 @@ def add_qr(ap, tty):
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without \033[33m--qr-pin\033[0m in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when \033[33m--qr-pin\033[0m is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
@@ -1427,16 +1496,18 @@ def add_upload(ap):
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see \033[33m--help-chmod\033[0m. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see \033[33m--help-chmod\033[0m. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication; see \033[33m--help-dedup\033[0m (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--redup", metavar="A[,B]=T", help="convert dedup-type for existing files; \033[33mA,B\033[0m is dedup-types to convert from (no/ref/sym/hard), \033[33mT\033[0m is target type; converting from sym/hard is fast-ish, from no/ref is slooow, to sym/hard is jank; see \033[33m--help-redup\033[0m. Example: [\033[32msym,hard=ref\033[0m] (volflag=redup)")
ap2.add_argument("--redup-dry", action="store_true", help="dry-run; makes \033[33m--redup\033[0m not apply changes (volflag=redup_dry)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
@@ -1530,7 +1601,7 @@ def add_auth(ap):
ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see \033[33m--help-idp\033[0m")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@@ -1542,7 +1613,7 @@ def add_auth(ap):
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see \033[33m--help-auth-ord\033[0m")
ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
@@ -1628,6 +1699,8 @@ def add_sftp(ap):
ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
ap2.add_argument("--sftp-fastldr", action="store_true", help="speedhack for 'loading hostkeys' on old CPUs; only use with known-good hostkeys")
ap2.add_argument("--sftp-chattr", metavar="T", default="s", help="which attributes can be modified with chattr? [\033[32ms\033[0m]=size, [\033[32mt\033[0m]=mtime, [\033[32mp\033[0m]=permissions, [\033[32mu\033[0m]=uid/gid. Example: [\033[32mstpu\033[0m]")
ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds")
@@ -1747,6 +1820,7 @@ def add_stats(ap):
def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--hack-me-bro", action="store_true", help="allow enabling dangerously buggy protocols such as \033[33m--smb\033[0m / \033[33m--tftp\033[0m")
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("--allow-svg-js", action="store_true", help="allow svg images to execute javascript; default-disabled because ~nobody wants it (volflag=allow_svg_js)")
@@ -1824,7 +1898,7 @@ def add_safety(ap):
ap2.add_argument("--csp-ui", metavar="TXT", default="script-src 'unsafe-eval' 'nonce-{{ js_nonce }}'; worker-src 'self'", help="content-security-policy to apply for the web-UI; default helps prevent XSS by blocking <script> / onclick / ... (volflag=csp_ui)")
ap2.add_argument("--csp-dl", metavar="TXT", default="", help="content-security-policy to apply for static files (volflag=csp_dl)")
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites; this will override \033[33m--csp-dl\033[0m with [\033[32mscript-src 'none'\033[0m] (volflag=noscript)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables --no-script (volflag=nohtml)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables \033[33m--no-script\033[0m (volflag=nohtml)")
ap2.add_argument("--no-mime", action="store_true", help="disallow changing the response mimetype with url-parameter ?mime=... (volflag=nomime)")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
@@ -1846,6 +1920,7 @@ def add_safety(ap):
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acao-re", metavar="PTN", type=u, default="", help="Access-Control-Allow-Origin; supplement to \033[33m--acao\033[0m; allow request if http-header 'Origin' matches either \033[33m--acao\033[0m or regex-\033[33mPTN\033[0m; ominous example: [\033[32m^moz-extension://.*\033[0m]")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
if FULL_HELP or ANYWIN:
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
@@ -1969,7 +2044,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/thumbs/diskimg.py\033[0m], see --help-thumb-ex")
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/thumbs/diskimg.py\033[0m], see \033[33m--help-thumb-ex\033[0m")
ap2.add_argument("--th-extr-sz", metavar="M", type=int, default=16, help="max num megabytes to allow \033[33m--th-extract\033[0m plugins to extract from each file")
ap2.add_argument("--hot-th-extr", action="store_true", help="recompile extractors on each thumbnail extraction -- expensive but convenient when hacking on stuff")
@@ -2126,7 +2201,7 @@ def add_ui(ap, retry: int):
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template; \033[32m<script>\033[0m will not work, use \033[32m<script nonce=\"{{ js_nonce }}\">\033[0m (volflag=html_head)")
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) \033[33m--html-head\033[0m but only accepts static text (volflag=html_head_s)")
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
@@ -2137,7 +2212,7 @@ def add_ui(ap, retry: int):
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as \033[33m--ver-who all\033[0m")
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
+2 -2
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 23)
VERSION = (1, 20, 26)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 9, 6)
BUILD_DT = (2026, 10, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+173 -7
View File
@@ -42,6 +42,7 @@ from .util import (
odfusion,
read_utf8,
relchk,
sanitize_fn,
statdir,
ub64enc,
uncyg,
@@ -107,6 +108,7 @@ SEESLOG = " (see fileserver log for details)"
SSEELOG = " ({})".format(SEE_LOG)
BAD_CFG = "invalid config; {}".format(SEE_LOG)
SBADCFG = " ({})".format(BAD_CFG)
REDUP_E2 = "WARNING: [/%s] needs 'e2ds' for redup from 'ref' and/or to 'sym'"
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
@@ -425,6 +427,7 @@ class VFS(object):
self.root = self
self.dev = 0 # st_dev
self.nodes: dict[str, VFS] = {} # child nodes
self.lnodes: Optional[dict[bytes, VFS]] = None # lowercase
self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
self.dbv: Optional[VFS] = None # closest full/non-jump parent
@@ -467,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
@@ -574,6 +578,11 @@ class VFS(object):
if name in self.nodes:
return self.nodes[name]._find(rem)
if self.lnodes:
zs = name.casefold().encode("ascii", "ignore")
if zs in self.lnodes:
return self.lnodes[zs]._find(rem)
return self, vpath
def can_access(
@@ -595,7 +604,7 @@ class VFS(object):
ret += "A"
return ret
def get(
def _get(
self,
vpath: str,
uname: str,
@@ -637,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:
@@ -672,8 +717,6 @@ class VFS(object):
if lfn == zs.lower():
hit = zs
break
if not hit:
return True # NFC/NFD or something, can't be helped either way
if self.log:
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
self.log("vfs", t % (fn, hit))
@@ -764,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,
@@ -785,6 +854,12 @@ class VFS(object):
if not rem:
# no vfs nodes in the list of real inodes
real = [x for x in real if x[0] not in self.nodes]
if self.lnodes:
real = [
x
for x in real
if x[0].casefold().encode("ascii", "ignore") not in self.lnodes
]
dbv = self.dbv or self
for name, vn2 in sorted(self.nodes.items()):
@@ -858,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
@@ -950,6 +1029,22 @@ class VFS(object):
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
yield d
def sanitize_fn(self, fn: str, rem: Any = None) -> str:
fn = sanitize_fn(fn, self.flags["fsnt"])
if rem:
return fn
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
return fn
def chk_fn(self, fn: str) -> None:
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
aps = ap + os.sep
if "xdev" in self.flags and not ANYWIN:
@@ -2915,6 +3010,17 @@ class AuthSrv(object):
if ccs == "y":
vol.flags["bcasechk"] = True
ptn = re.compile(r"^(,no|,sym|,hard|,ref)+=(sym|hard|ref)$")
ptn2 = re.compile(r"(no|ref).*=|=sym")
for vol in vfs.all_nodes.values():
zs = vol.flags.get("redup")
if zs and not ptn.match("," + zs):
t = "invalid redup for volume /%s: %r"
self.log(t % (vol.vpath, zs), 1)
errors = True
if zs and "e2ds" not in vol.flags and ptn2.search(zs):
self.log(REDUP_E2 % (vol.vpath,), 3)
tags = self.args.mtp or []
tags = [x.split("=")[0] for x in tags]
tags = [y for x in tags for y in x.split(",")]
@@ -2940,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
@@ -2957,6 +3071,9 @@ class AuthSrv(object):
errors = True
for vol in vfs.all_nodes.values():
for zs in vf_bmap().values():
if vol.flags.get(zs) in (None, False, ""):
vol.flags.pop(zs, None)
for k in list(vol.flags.keys()):
if re.match("^-[^-]+$", k):
vol.flags.pop(k)
@@ -2971,6 +3088,22 @@ class AuthSrv(object):
for name in vol.axs.uread:
vol.axs.udot.add(name)
zi = 1
zs = "; you must also enable --hack-me-bro to accept the total loss of security"
if self.args.hack_me_bro:
zi = 3
zs = ""
if self.args.smb:
t = "smb is enabled; this has serious security issues which will not be fixed"
self.log(t + zs, zi)
if zs:
errors = True
if self.args.tftp:
t = "tftp is enabled; primitive protocol with primitive access-restrictions"
self.log(t, 3)
if errors:
sys.exit(1)
@@ -3275,6 +3408,39 @@ class AuthSrv(object):
cur.close()
db.close()
zb1 = zb2 = False
nodes = [self.vfs]
while nodes:
vn = nodes.pop()
nodes += list(vn.nodes.values())
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 ".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 zb2 and PY2:
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
self.log(t, 1)
if zb2:
t = "SECURITY WARNING: you have a case-insensitive / casefolding filesystem, and you also have nested volumes (subvolumes). This combination is NOT 100% bulletproof; it *might* be possible for someone to sneakily access a subvolume they're not supposed to have access to, if they have access to the parent volume."
self.log(t, 3)
if zb1:
t = "you have a case-insensitive / casefolding filesystem, so it is recommended to set --hist C:\\copyparty-data\\ or another safe place"
self.log(t, 3)
self.js_ls = {}
self.js_htm = ""
self.md_htm = ""
@@ -3294,7 +3460,7 @@ class AuthSrv(object):
"dth3x": vf["th3x"],
"u2ts": vf["u2ts"],
"shr_who": vf["shr_who"],
"frand": bool(vf.get("rand")),
"frand": "rand" in vf,
"lifetime": vf.get("lifetime") or 0,
"unlist": vf.get("unlist") or "",
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
@@ -3352,7 +3518,7 @@ class AuthSrv(object):
"u2sz": self.args.u2sz,
"u2ts": vf["u2ts"],
"u2ow": vf["u2ow"],
"frand": bool(vf.get("rand")),
"frand": "rand" in vf,
"lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort,
}
+4
View File
@@ -66,6 +66,7 @@ def vf_bmap() -> dict[str, str]:
"og_s_title",
"opds",
"rand",
"redup_dry",
"reflink",
"rm_partial",
"rmagic",
@@ -146,6 +147,7 @@ def vf_vmap() -> dict[str, str]:
"put_ck",
"put_name",
"readmes",
"redup",
"rotf_tz",
"mv_retry",
"rm_retry",
@@ -229,6 +231,8 @@ flagcats = {
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
"safededup": "verify on-disk data before using it for dedup",
"redup=A,B=T": "convert dedup-type for existing files",
"redup_dry": "dry-run mode for redup",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"nodupem": "rejects existing files during moves as well",
+20 -1
View File
@@ -10,7 +10,7 @@ import time
from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, json_hesc, min_ex, undot
from .util import chkcmd, fsenc, json_hesc, min_ex, runcmd, undot
if True: # pylint: disable=using-constant-test
from typing import Optional, Union
@@ -18,6 +18,11 @@ if True: # pylint: disable=using-constant-test
from .util import RootLogger, undot
PTN_FILEFRAG = re.compile(
r"^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):\s*[0-9]+:\s*(.*)$"
)
class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
self.log_func = log
@@ -264,3 +269,17 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
vol.js_htm = json_hesc(json.dumps(htm))
# md_htm is dontcare; only relevant for up2k request body
def filefrag(ap: str) -> list[str]:
rc, so, se = runcmd([b"filefrag", b"-v", fsenc(ap)], timeout=10)
ptn = PTN_FILEFRAG
ret = []
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
a, b, s = m.groups()
if "shared" in s.strip().split(","):
ret.append((a, b))
return ret
+22 -23
View File
@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.servers import FTPServer
from .__init__ import PY2, TYPE_CHECKING
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import (
@@ -29,7 +29,6 @@ from .util import (
ipnorm,
pybin,
read_utf8,
relchk,
runhook,
sanitize_fn,
set_fperms,
@@ -169,16 +168,23 @@ class FtpFs(AbstractedFS):
d: bool = False,
) -> tuple[str, VFS, str]:
try:
vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
logging.warning("malicious vpath: %s", vpath)
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
# because:
# * ftp-spec doesn't define sep
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vfs.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vfs.flags["emb_all"]
@@ -193,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)
@@ -314,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
+57 -33
View File
@@ -17,6 +17,7 @@ import threading # typechk
import time
import uuid
from datetime import datetime
from hmac import compare_digest as ct_eq
from operator import itemgetter
import jinja2 # typechk
@@ -104,13 +105,11 @@ from .util import (
read_socket_chunked,
read_socket_unbounded,
read_utf8,
relchk,
ren_open,
runhook,
s2hms,
s3enc,
safe_mime,
sanitize_fn,
sanitize_vpath,
sendfile_kern,
sendfile_py,
@@ -738,6 +737,7 @@ class HttpCli(object):
hpw = self.asrv.ah.hash(bauth)
if self.asrv.iacct.get(hpw):
break
bauth = zs
except:
pass
@@ -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
@@ -1378,6 +1377,7 @@ class HttpCli(object):
if (
self.args.pw_hdr in ih
or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins
or (self.args.acao_re and self.args.acao_re.match(origin))
):
good_origin = True
bad_hdrs = ("",)
@@ -2474,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")
@@ -2734,7 +2737,7 @@ class HttpCli(object):
if rnd:
fn = rand_name(fdir, fn, rnd)
fn = sanitize_fn(fn or "")
fn = vfs.sanitize_fn(fn or "", rem)
path = os.path.join(fdir, fn)
@@ -3214,19 +3217,20 @@ class HttpCli(object):
if "delete" in self.uparam:
return self.handle_rm(body)
name = undot(body["name"])
if "/" in name:
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
name = body["name"]
if "/" in name or name in (".", ".."):
raise Pebkac(400, "filename contains '/' or is invalid")
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
fsnt = vfs.flags["fsnt"]
if fsnt != "lin":
vpath = self.vpath
fsnt = self.vn.flags["fsnt"]
if not VPTL_OS and fsnt != "lin":
tl = VPTL_WIN if fsnt == "win" else VPTL_MAC
rem = rem.translate(tl)
name = name.translate(tl)
vpath = vpath.translate(tl)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
name = vfs.sanitize_fn(name, rem)
dbv, vrem = vfs.get_dbv(rem)
name = sanitize_fn(name)
if (
not self.can_read
and self.can_write
@@ -3251,6 +3255,8 @@ class HttpCli(object):
dst = vfs.canonical(rem)
try:
if not bos.path.isdir(dst):
if "nosub" in vfs.flags:
raise Pebkac(500, "no subdirectories allowed")
bos.makedirs(dst, vf=vfs.flags)
except OSError as ex:
self.log("makedirs failed %r" % (dst,))
@@ -3262,6 +3268,8 @@ class HttpCli(object):
raise Pebkac(400, "some file got your folder name")
raise Pebkac(500, min_ex())
except Pebkac:
raise
except:
raise Pebkac(500, min_ex())
@@ -3587,7 +3595,6 @@ class HttpCli(object):
return self.tx_404()
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, False, True)
vpath = vjoin(vfs.vpath, rem)
ap = vfs.canonical(rem)
st = bos.stat(ap)
@@ -3770,12 +3777,11 @@ class HttpCli(object):
def _mkdir(self, vpath: str, dav: bool = False) -> bool:
nullwrite = self.args.nw
self.gctx = vpath
vpath = sanitize_vpath(undot(vpath))
vpath = sanitize_vpath(undot(vpath), self.vn.flags["fsnt"])
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
rem = sanitize_vpath(rem)
fn = vfs.canonical(rem)
if not nullwrite:
@@ -3822,10 +3828,12 @@ class HttpCli(object):
t = "you can only create %s files because you don't have the delete-permission"
raise Pebkac(400, t % (vfs.flags["rw_edit"].replace(",", "/")))
sanitized = sanitize_fn(new_file)
sanitized = vfs.sanitize_fn(new_file, rem)
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:
@@ -3888,7 +3896,7 @@ class HttpCli(object):
rnd = 0
else:
rnd = int(self.uparam.get("rand") or self.headers.get("rand") or 0)
if vfs.flags.get("rand"): # force-enable
if "rand" in vfs.flags: # force-enable
rnd = max(rnd, vfs.flags["nrand"])
zs = self.uparam.get("life", self.headers.get("life", ""))
@@ -3977,12 +3985,19 @@ class HttpCli(object):
# fallthrough
fdir = fdir_base
fname = sanitize_fn(p_file or "")
fname = vfs.sanitize_fn(p_file or "", rem)
suffix = "-%.6f-%s" % (time.time(), dip)
if p_file and not nullwrite:
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}
@@ -4304,6 +4319,7 @@ class HttpCli(object):
else:
self.redirect(
self.vpath,
suf="?b=u" if self.ouparam.get("b") == "u" else "",
msg=msg + suf,
flavor="return to",
click=False,
@@ -4339,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 (
@@ -4357,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:
@@ -4582,7 +4600,7 @@ class HttpCli(object):
ap = vn.canonical(self.rem)
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]
if req == zs:
if ct_eq(req, zs):
return True
t = "wrong dirkey, want %s, got %s\n vp: %r\n ap: %r"
@@ -4610,7 +4628,7 @@ class HttpCli(object):
alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
)[:fk_len]
if req == zs:
if ct_eq(req, zs):
return True
t = "wrong filekey, want %s, got %s\n vp: %r\n ap: %r"
@@ -5263,6 +5281,8 @@ class HttpCli(object):
self.send_headers("oh_f", length=upper - lower, status=status, mime=mime)
wr_slp = self.args.s_wr_slp
wr_sz = self.args.s_wr_sz
dls = self.conn.hsrv.dls
dl_id = self.dl_id
file_size = job["size"]
chunk_size = up2k_chunksize(file_size)
num_need = -1
@@ -5282,7 +5302,7 @@ class HttpCli(object):
if job:
self.pipes.set(req_path, job)
if not job:
if not job or not job["hash"]:
t = "pipe: OK, upload has finished; yeeting remainder"
self.log(t, 2)
data_end = file_size
@@ -5379,6 +5399,9 @@ class HttpCli(object):
broken = True
break
if dl_id:
dls[dl_id] = (time.time(), lower)
if lower < upper and not broken:
with open_nolock(req_path, "rb") as f:
remains = sendfile_py(
@@ -5390,8 +5413,8 @@ class HttpCli(object):
wr_sz,
wr_slp,
not self.args.no_poll,
self.conn.hsrv.dls,
self.dl_id,
dls,
dl_id,
)
spd = self._spd((upper - lower) - remains)
@@ -6660,6 +6683,7 @@ class HttpCli(object):
act = self.uparam["eshare"]
if act == "rm":
cur.execute("delete from sh where k = ?", (skey,))
cur.execute("delete from sf where k = ?", (skey,))
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
reload = True
else:
@@ -7079,17 +7103,16 @@ class HttpCli(object):
e2d = "e2d" in vn.flags
e2t = "e2t" in vn.flags
og_fn = ""
add_og = "og" in vn.flags
if add_og:
if "th" in self.uparam or "raw" in self.uparam or "opds" in self.uparam:
add_og = False
elif vn.flags["og_ua"]:
add_og = vn.flags["og_ua"].search(self.ua)
og_fn = ""
if "v" in self.uparam:
add_og = True
og_fn = ""
if "b" in self.uparam and "norobots" not in vn.flags:
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
@@ -7261,7 +7284,7 @@ class HttpCli(object):
if use_dirkey:
is_dk = True
elif self.can_get and "doc" in self.uparam:
zs = vjoin(self.vpath, self.uparam["doc"]) + "?v"
zs = vjoin(self.vpath, self.uparam["doc"].split("//")[-1]) + "?v"
return self.redirect(zs, flavor="redirecting to", use302=True)
elif not self.can_write:
return self.tx_404(True)
@@ -7610,6 +7633,7 @@ class HttpCli(object):
return self.tx_file("oh_f", ap) # is no-cache
if icur:
assert idx # type: ignore # !rm
mte = vn.flags.get("mte") or {}
tagset: set[str] = set()
rd = vrem
@@ -7978,7 +8002,7 @@ class HttpCli(object):
else:
j2a["og_url"] = j2a["og_raw"] = url_base
if not vn.flags.get("og_no_head"):
if "og_no_head" not in vn.flags:
ogh = {"twitter:card": "summary"}
title = str(vn.flags.get("og_title") or "")
@@ -8047,7 +8071,7 @@ class HttpCli(object):
while title.endswith(" - "):
title = title[:3]
if vn.flags.get("og_s_title") or not title:
if "og_s_title" in vn.flags or not title:
title = str(vn.flags.get("og_title") or "")
for tag, hname in tagmap.items():
+54 -26
View File
@@ -37,7 +37,6 @@ from .util import (
ipnorm,
min_ex,
read_utf8,
relchk,
runhook,
sanitize_fn,
ub64enc,
@@ -55,6 +54,23 @@ if True: # pylint: disable=using-constant-test
SATTR = paramiko.sftp_attr.SFTPAttributes
try:
from paramiko.rsakey import serialization
_load_der_pk = serialization.load_der_private_key
def fastloader(*a, **ka):
ka0 = ka.copy()
try:
ka["unsafe_skip_rsa_key_validation"] = True
return _load_der_pk(*a, **ka)
except:
return _load_der_pk(*a, **ka0)
except:
pass
class SSH_Srv(paramiko.ServerInterface):
def __init__(self, hub: "SvcHub", addr: Any):
self.hub = hub
@@ -295,6 +311,17 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
if self.uname == LEELOO_DALLAS:
raise Exception("send her back")
cm = 0
for zs, zi in (
("s", SATTR.FLAG_SIZE),
("t", SATTR.FLAG_AMTIME),
("p", SATTR.FLAG_PERMISSIONS),
("u", SATTR.FLAG_UIDGID),
):
if zs in self.args.sftp_chattr:
cm |= zi
self.chattr_mask = cm
self.vols = [
vp
for vp, vn in self.asrv.vfs.all_vols.items()
@@ -320,15 +347,23 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
vpath = vpath.replace(os.sep, "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
self.log("malicious vpath: %s", vpath)
raise Exception("Unsupported characters in [%s]" % (vpath,))
# because:
# * maybe some clients use \
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vn.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vn.flags["emb_all"]
@@ -343,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")
@@ -667,8 +691,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _mkdir(self, vp: str, attr: SATTR) -> int:
self.log("mkdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
ap = vn.canonical(rem, False)
ap, vn, _ = self.v2a(vp, w=True)
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
if attr is not None:
paramiko.SFTPServer.set_file_attr(ap, attr)
@@ -691,8 +714,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _rmdir(self, vp: str) -> int:
self.log("rmdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
ap = os.path.join(vn.realpath, rem)
ap, _, _ = self.v2a(vp, d=True)
bos.rmdir(ap)
return SFTP_OK
except Pebkac as ex:
@@ -713,8 +735,8 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _chattr(self, vp: str, attr: SATTR) -> int:
self.log("chattr(%s, %s)" % (vp, attr))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
ap = os.path.join(vn.realpath, rem)
attr._flags &= self.chattr_mask
ap, _, _ = self.v2a(vp, w=True, d=True)
paramiko.SFTPServer.set_file_attr(ap, attr)
return SFTP_OK
except Pebkac as ex:
@@ -774,6 +796,10 @@ class Sftpd(object):
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
return
if args.sftp_fastldr:
serialization.load_der_private_key = fastloader # type: ignore
else:
self.log("loading hostkeys...")
self.hostkeys = []
hostkeytypes = (
("ed25519", "Ed25519Key", {}), # best
@@ -801,6 +827,8 @@ class Sftpd(object):
self.hostkeys.append(pkey)
if args.sftpv:
self.log("loaded hostkey %r" % (pkey,))
if args.sftp_fastldr:
serialization.load_der_private_key = _load_der_pk # type: ignore
ips = list(ODict.fromkeys(ips)) # dedup
+8 -1
View File
@@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import ctypes
import sys
from ctypes import wintypes
if True: # pylint: disable=using-constant-test
@@ -9,6 +10,9 @@ if True: # pylint: disable=using-constant-test
from . import _shared as shared
WINVER = sys.getwindowsversion()
LE_XP = WINVER.major < 6
NO_ERROR = 0
ERROR_BUFFER_OVERFLOW = 111
MAX_ADAPTER_NAME_LENGTH = 256
@@ -83,7 +87,10 @@ def enumerate_interfaces_of_adapter(
t = "sockaddr_to_ip({}) returned None"
raise Exception(t.format(address.Address.lpSockaddr))
network_prefix = address.OnLinkPrefixLength
if LE_XP:
network_prefix = 16 if str(ip).startswith("169.254") else 24
else:
network_prefix = address.OnLinkPrefixLength
yield shared.IP(ip, network_prefix, nice_name)
+19 -28
View File
@@ -174,6 +174,7 @@ class SvcHub(object):
self.is_dut = False # running in unittest; always False
self.stopping = False
self.stopped = False
self.init_mutex = threading.Lock()
self.reload_mutex = threading.Lock()
self.retcode = 0
self.httpsrv_up = 0
@@ -535,9 +536,6 @@ class SvcHub(object):
self.tftpd: Optional[Tftpd] = None
if args.sftp or args.ftp or args.ftps or args.tftp:
Daemon(self.start_ftpd, "start_tftpd")
if args.smb:
# impacket.dcerpc is noisy about listen timeouts
sto = socket.getdefaulttimeout()
@@ -861,29 +859,19 @@ class SvcHub(object):
cur.close()
db.close()
def start_ftpd(self) -> None:
time.sleep(30)
if hasattr(self, "sftpd") and not self.sftpd:
self.restart_sftpd()
if hasattr(self, "ftpd") and not self.ftpd:
self.restart_ftpd()
if hasattr(self, "tftpd") and not self.tftpd:
self.restart_tftpd()
def restart_sftpd(self) -> None:
if not hasattr(self, "sftpd"):
return
from .sftpd import Sftpd
if self.sftpd:
return # todo
with self.init_mutex:
if self.sftpd:
return # todo
self.sftpd = Sftpd(self)
self.sftpd.run()
self.sftpd = Sftpd(self)
self.sftpd.run()
self.log("root", "started SFTPd")
def restart_ftpd(self) -> None:
@@ -892,13 +880,15 @@ class SvcHub(object):
from .ftpd import Ftpd
if self.ftpd:
return # todo
with self.init_mutex:
if self.ftpd:
return # todo
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
self.ftpd = Ftpd(self)
self.ftpd = Ftpd(self)
self.log("root", "started FTPd")
def restart_tftpd(self) -> None:
@@ -907,10 +897,11 @@ class SvcHub(object):
from .tftpd import Tftpd
if self.tftpd:
return # todo
with self.init_mutex:
if self.tftpd:
return # todo
self.tftpd = Tftpd(self)
self.tftpd = Tftpd(self)
def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5)
@@ -1290,7 +1281,7 @@ class SvcHub(object):
TH_BWRAP[:] = al.th_bwrap_b
zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
zs = "acao_re dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k)
if not vs or vs == "no":
+1 -1
View File
@@ -104,7 +104,7 @@ def gen_hdr(
ret += spack(b"<LL", vsz, vsz)
# windows support (the "?" replace below too)
fn = sanitize_to(fn, VPTL_WIN)
fn = sanitize_to(fn, VPTL_WIN, "win")
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
+3
View File
@@ -87,6 +87,9 @@ class TcpSrv(object):
successful_binds = 0
try:
for ipa in ips:
if ipa == "no":
self.args.ign_ebind_all = True
continue
try:
self._listen(ipa, port)
ok[ipa].append(port)
+5 -1
View File
@@ -275,7 +275,11 @@ class Tftpd(object):
if not vfs.realpath:
raise Exception("unmapped vfs")
return vfs, vpath, vfs.canonical(rem)
ap = vfs.canonical(rem)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise Exception("file not found")
return vfs, vpath, ap
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
# generate file listing if vpath is dir.txt and return as file object
+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
@@ -201,11 +201,11 @@ try:
# reduces glibc RAM usage from 4.7 to 3.5 GiB ...yep, still bonkers
os.environ["VIPS_CONCURRENCY"] = "1"
HAVE_VIPS = True
import pyvips
pyvips.cache_set_max(0)
logging.getLogger("pyvips").setLevel(logging.WARNING)
HAVE_VIPS = True
except Exception as e:
HAVE_VIPS = False
if not isinstance(e, ImportError):
@@ -219,10 +219,10 @@ try:
if PRTY_NO_RAWPY:
raise Exception()
HAVE_RAWPY = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
HAVE_RAWPY = True
except:
HAVE_RAWPY = False
+207 -46
View File
@@ -17,10 +17,10 @@ from copy import deepcopy
from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, UNIX, WINDOWS, E
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
from .authsrv import LEELOO_DALLAS, REDUP_E2, SEESLOG, VFS, AuthSrv
from .bos import bos
from .cfg import vf_bmap, vf_cmap, vf_vmap
from .fsutil import Fstab
from .fsutil import Fstab, filefrag
from .mtag import MParser, MTag
from .util import (
E_FS_CRIT,
@@ -887,13 +887,10 @@ class Up2k(object):
def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
if not self.no_expr_idx:
return False, flags
return False, dict(flags)
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
if len(ret) == len(flags):
return False, flags
return True, ret
return len(ret) != len(flags), flags
def init_indexes(
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
@@ -912,7 +909,7 @@ class Up2k(object):
with self.mutex:
if gid != self.gid:
return False
return
if self.pp:
continue
@@ -951,6 +948,9 @@ class Up2k(object):
with self.mutex, self.reg_mutex:
# only need to protect register_vpath but all in one go feels right
for vol in vols:
if vol.realpath:
self.flags[vol.realpath] = dict(vol.flags)
if bos.path.isfile(vol.realpath):
self.volstate[vol.vpath] = "online (just-a-file)"
t = "NOTE: volume [/%s] is a file, not a folder"
@@ -1000,6 +1000,21 @@ class Up2k(object):
with self.mutex, self.reg_mutex:
self._drop_caches()
def setstate(vol, n):
t = ""
if n < 1 and "e2v" in vol.flags:
t += "+e2v"
if n < 2 and vol.flags.get("redup"):
t += "+redup"
if n < 3 and "e2ts" in vol.flags:
t += "+tags"
if t:
t = "online (%s pending)" % (t[1:],)
else:
t = "online, idle"
self.volstate[vol.vpath] = t
for vol in vols:
if self.stop or gid != self.gid:
break
@@ -1016,26 +1031,16 @@ class Up2k(object):
if vac:
need_vac[vol] = True
if "e2v" in vol.flags:
t = "online (integrity-check pending)"
elif "e2ts" in vol.flags:
t = "online (tags pending)"
else:
t = "online, idle"
self.volstate[vol.vpath] = t
setstate(vol, 0)
self._unblock()
# file contents verification
for vol in vols:
if self.stop:
break
if "e2v" not in vol.flags:
if self.stop or "e2v" not in vol.flags:
continue
t = "online (verifying integrity)"
t = "online (doing e2v)"
self.volstate[vol.vpath] = t
self.log("{} [{}]".format(t, vol.realpath))
@@ -1044,12 +1049,23 @@ class Up2k(object):
self.log("modified {} entries in the db".format(nmod), 3)
need_vac[vol] = True
if "e2ts" in vol.flags:
t = "online (tags pending)"
else:
t = "online, idle"
setstate(vol, 1)
# convert dedup type
for vol in vols:
if self.stop or not vol.flags.get("redup"):
continue
t = "online (doing redup)"
self.volstate[vol.vpath] = t
self.log("{} [{}]".format(t, vol.realpath))
try:
with self.mutex, self.reg_mutex:
self._redup(vol, vols)
except:
self.log("redup failed: " + min_ex(), 1)
setstate(vol, 2)
# open the rest + do any e2ts(a)
needed_mutagen = False
@@ -1084,7 +1100,7 @@ class Up2k(object):
cur.execute("vacuum")
if self.stop:
return False
return
for vol in all_vols.values():
if vol.flags["dbd"] == "acid":
@@ -1114,7 +1130,7 @@ class Up2k(object):
self.log("checkpoint failed: {}".format(ex), 3)
if self.stop:
return False
return
self.pp.end = True
@@ -1158,12 +1174,12 @@ class Up2k(object):
vpath = k
_, flags = self._expr_idx_filter(flags)
n4g = bool(flags.get("noforget"))
n4g = "noforget" in flags
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())
@@ -1297,11 +1313,14 @@ class Up2k(object):
self.log(t % (vpath, ex), 1)
return None
if dir_is_empty(self.log_func, not self.args.no_scandir, histpath) and not (
ANYWIN or UNIX or "hist_cow" in flags
if not (
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
):
try:
runcmd([b"chattr", b"+C", fsenc(histpath)], 1)
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
except:
pass
@@ -1388,14 +1407,15 @@ class Up2k(object):
top = vol.realpath
rei = vol.flags.get("noidx")
reh = vol.flags.get("nohash")
n4g = bool(vol.flags.get("noforget"))
n4g = "noforget" in vol.flags
ffat = "fat32" in vol.flags
cst = bos.stat(top)
dev = cst.st_dev if vol.flags.get("xdev") else 0
dev = cst.st_dev if "xdev" in vol.flags else 0
with self.mutex:
with self.reg_mutex:
reg = self.register_vpath(top, vol.flags)
self.flags[vol.realpath] = dict(vol.flags)
assert reg and self.pp # !rm
cur, db_path = reg
@@ -1450,7 +1470,7 @@ class Up2k(object):
[],
cst,
dev,
bool(vol.flags.get("xvol")),
"xvol" in vol.flags,
)
if not n4g:
n_rm = self._drop_lost(db.c, top, excl)
@@ -2088,6 +2108,138 @@ class Up2k(object):
return len(rewark) + len(f404)
def _redup(self, vol: VFS, vols: list[VFS]) -> None:
logmsg = "redup: replacing %r with %r"
dry = "redup_dry" in vol.flags
if dry:
logmsg += " #dry"
zs, to = vol.flags["redup"].split("=")
zsl = zs.split(",")
cref = "no" in zsl or "ref" in zsl
csym = "sym" in zsl
chard = "hard" in zsl
chard0 = chard
vf = vol.flags.copy()
vf["dedup"] = 1
for zs in "hardlink hardlinkonly reflink".split():
vf.pop(zs, None)
if to == "ref":
vf["reflink"] = 1
elif to == "hard":
vf["hardlinkonly"] = vf["hardlink"] = 1
elif to == "sym":
pass
else:
raise Exception("target type not implemented: " + to)
if (csym or chard) and to != "sym":
self.log("redup: stage 1 begin")
if to == "hard":
chard = False
for x in vol.walk("", "", [], LEELOO_DALLAS, [[]], 2, True, True, True):
vn, _, _, atop, lsf, lsd, lsv = x
if vn is not vol:
lsd[:] = []
lsv.clear()
continue
for fn, st in lsf:
if (csym and stat.S_ISLNK(st.st_mode)) or (
chard and st.st_nlink > 1
):
ap = os.path.join(atop, fn)
self.log(logmsg % (ap, absreal(ap)))
if dry:
continue
ap2 = tempfile.NamedTemporaryFile(
prefix="r,", dir=atop, delete=False
).name
self._symlink(ap, ap2, vf, True, True, st.st_mtime)
st1 = bos.stat(ap)
st2 = bos.lstat(ap2)
if st1.st_size != st2.st_size or stat.S_ISLNK(st2.st_mode):
wunlink(self.log, ap2, vf)
raise Exception("redup: clone failed; giving up")
wunlink(self.log, ap, vf)
bos.rename(ap2, ap)
if cref or to == "sym":
self.log("redup: stage 2 begin")
if "e2ds" not in vol.flags:
self.log(REDUP_E2 % (vol.vpath,), 3)
cur = self.cur[vol.realpath]
curs = [(vol, cur.connection.cursor())]
if to != "sym":
for v2 in vols:
if vol is not v2 and v2.realpath in self.cur:
curs += [(v2, self.cur[v2.realpath])]
nrem = cur.execute("select count(w) from up").fetchone()[0]
self.log("redup [/%s]: %d files left" % (vol.vpath, nrem))
q2 = "select rd, fn from up where substr(w,1,16) = ? and w=?"
for w, rd, fn in cur.execute("select w, rd, fn from up"):
w16 = w[:16]
for v2, c2 in curs:
hits = c2.execute(q2, (w16, w)).fetchall()
if not hits:
continue
rd2, fn2 = hits[0]
if fn == fn2 and rd == rd2 and vol is v2:
continue
apt = ""
try:
rd, fn = s3dec(rd, fn)
rd2, fn2 = s3dec(rd2, fn2)
fp1 = os.path.join(vol.realpath, rd, fn)
fp2 = os.path.join(v2.realpath, rd2, fn2)
st1 = bos.lstat(fp1)
if fp1 == fp2 or stat.S_ISLNK(st1.st_mode):
continue # self(?), or redup failed during walk
if to == "hard" and st1.st_nlink >= len(hits):
continue
ap1 = absreal(fp1)
ap2 = absreal(fp2)
st2 = bos.lstat(ap2)
if stat.S_ISLNK(st2.st_mode):
continue # dead hit
if to == "ref" and ap1 != ap2:
try:
frag1 = filefrag(ap1)
if frag1:
frag2 = filefrag(ap2)
if frag1 == frag2:
continue
except Exception as ex:
self.log("redup: filefrag failed (OK); %r" % (ex,))
for ap in (ap1,) if ap1 == ap2 else (ap1, ap2):
self.log("redup: integrity-checking %r" % (ap,))
zsl, st = self._hashlist_from_file(ap)
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
if w2 != w:
t = "db desync:\n%s %r\n%s db"
raise Exception(t % (w2, ap, w))
self.log(logmsg % (ap1, ap2))
if dry:
continue
apt = tempfile.NamedTemporaryFile(
prefix="r,", dir=os.path.dirname(fp1), delete=False
).name
self._symlink(ap2, apt, vf, True, True, st1.st_mtime)
zsl, st = self._hashlist_from_file(apt)
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
if w != w2:
raise Exception("bad checksum after copy?!")
wunlink(self.log, ap1, vf)
bos.rename(apt, ap1)
break
except Exception as ex:
self.log("redup: skipping match due to " + str(ex))
if apt and os.path.exists(apt):
os.unlink(apt)
continue
def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:
ptop = vol.realpath
with self.mutex, self.reg_mutex:
@@ -3075,17 +3227,20 @@ class Up2k(object):
jcur = self.cur.get(ptop)
reg = self.registry[ptop]
vfs = self.vfs.all_vols[cj["vtop"]]
n4g = bool(vfs.flags.get("noforget"))
noclone = bool(vfs.flags.get("noclone"))
rand = vfs.flags.get("rand") or cj.get("rand")
n4g = "noforget" in vfs.flags
noclone = "noclone" in vfs.flags
rand = "rand" in vfs.flags or cj.get("rand")
lost: list[tuple["sqlite3.Cursor", str, str]] = []
safe_dedup = vfs.flags.get("safededup") or 50
data_ok = safe_dedup < 10 or n4g
vols = [(ptop, jcur)] if jcur else []
if vfs.flags.get("xlink"):
if vols and ("xlink" in vfs.flags or "reflink" in vfs.flags):
vols += [(k, v) for k, v in self.cur.items() if k != ptop]
if "xlink" not in vfs.flags:
zs = self.fstab.get(ptop)[1]
vols = [x for x in vols if self.fstab.get(x[0])[1] == zs]
if noclone:
wark = up2k_wark_from_metadata(
@@ -3395,7 +3550,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4))
@@ -3575,7 +3730,7 @@ class Up2k(object):
st = bos.stat(fp)
try:
vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink"))
xlink = "xlink" in vf
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
except Exception as ex:
@@ -3630,7 +3785,7 @@ class Up2k(object):
if rm and bos.path.exists(dst):
wunlink(self.log, dst, flags)
if not is_mv and not flags.get("dedup"):
if not is_mv and "dedup" not in flags:
raise Exception("dedup is disabled in config")
if "reflink" in flags:
@@ -4254,7 +4409,7 @@ class Up2k(object):
is_dir = stat.S_ISDIR(st.st_mode)
except:
# NOTE: "file not found" *sftpd
raise Pebkac(400, "file not found on disk (already deleted?)")
raise Pebkac(404, "file not found on disk (already deleted?)")
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
raise Pebkac(400, "file does not exist case-sensitively")
@@ -4331,7 +4486,7 @@ class Up2k(object):
cur = None
try:
ptop = dbv.realpath
xlink = bool(dbv.flags.get("xlink"))
xlink = "xlink" in dbv.flags
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file(
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
@@ -4535,6 +4690,12 @@ class Up2k(object):
self.log("not found in src db: %r" % (svp,))
try:
if svn_dbv != dvn and not (
"xlink" in dvn.flags
or "reflink" in dvn.flags
or "hardlinkonly" in dvn.flags
):
raise OSError(errno.EXDEV, "different volumes")
if is_link and st != stl:
# relink non-broken symlinks to still work after the move,
# but only resolve 1st level to maintain relativity
@@ -4812,7 +4973,7 @@ class Up2k(object):
raise Pebkac(400, t)
self._copy_tags(c1, c2, w)
xlink = bool(svn.flags.get("xlink"))
xlink = "xlink" in svn.flags
with self.reg_mutex:
has_dupes = self._forget_file(
@@ -5278,7 +5439,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
job["vcfg"] = vf
+61 -24
View File
@@ -188,6 +188,7 @@ try:
HAVE_IFADDR = True
except:
HAVE_IFADDR = False
IFADDR_VND = False
def get_adapters(include_unconfigured=False):
return []
@@ -288,7 +289,6 @@ try:
raise Exception()
socket.inet_pton(socket.AF_INET6, "::1")
HAVE_IPV6 = True
if GRAAL:
try:
@@ -303,13 +303,15 @@ try:
return _inet_pton(fam, ip)
socket.inet_pton = inet_pton
HAVE_IPV6 = True
except:
HAVE_IPV6 = False
def inet_pton(fam, ip):
return socket.inet_aton(ip)
socket.inet_pton = inet_pton
HAVE_IPV6 = False
try:
@@ -363,13 +365,19 @@ if PY2:
else:
umktrans = str.maketrans
FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/")
VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\")
APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/")
FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":")
FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None
VPTL_MAC = umktrans(":", ":")
VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None
APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None
BADNAMES = []
if ANYWIN:
BADNAMES += ["con", "prn", "aux", "nul"]
BADNAMES += ["com%s" % (n,) for n in range(1, 10)]
BADNAMES += ["lpt%s" % (n,) for n in range(1, 10)]
BADNAMES += ["com%s" % (n,) for n in ("¹", "²", "³")]
BADNAMES += ["lpt%s" % (n,) for n in ("¹", "²", "³")]
BADNAMESET = set(BADNAMES)
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@@ -450,6 +458,7 @@ IMPLICATIONS = [
["no_logue", "no_logues"], # user-typo
["nologues", "no_logues"], # user-typo
["nologue", "no_logues"], # user-typo
["nw", "redup_dry"],
["plainlogue", "plainlogues"], # user-typo
["plainreadmes", "plainreadme"], # user-typo
["noscript", "plainlogues"],
@@ -460,6 +469,8 @@ IMPLICATIONS = [
["smbvvv", "smbvv"],
["smbvv", "smbv"],
["smbv", "smb"],
["xdev", "xvd"],
["xvol", "xvd"],
["zv", "zmv"],
["zv", "zsv"],
["z", "zm"],
@@ -2426,7 +2437,7 @@ def uncyg(path: str) -> str:
return "%s:\\%s" % (path[1], path[3:])
def undot(path: str) -> str:
def _undot(path: str) -> str:
ret: list[str] = []
for node in path.split("/"):
if node == "." or not node:
@@ -2442,30 +2453,56 @@ def undot(path: str) -> str:
return "/".join(ret)
def sanitize_fn(fn: str) -> str:
fn = fn.replace("\\", "/").split("/")[-1]
if APTL_OS:
fn = sanitize_to(fn, APTL_OS)
def _undot_win(path: str) -> str:
ret: list[str] = []
for node in path.replace("\\", "/").split("/"):
if node == "..":
if ret:
ret.pop()
continue
node = node.rstrip(". ")
if node:
ret.append(node)
return "/".join(ret)
if ANYWIN:
undot = _undot_win
else:
undot = _undot
def sanitize_fn(fn: str, fsnt: Optional[str] = None) -> str:
fn = fn.replace("\x00", "_").split("/")[-1]
if fsnt and fsnt != "lin":
fn = fn.translate(VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC)
fn2 = fn.replace("\\", "/")
if ANYWIN or fsnt == "win" or "/../" in fn2:
fn = fn2.split("/")[-1].rstrip(". ")
if VPTL_OS:
fn = sanitize_to(fn, VPTL_OS)
return fn.strip()
def sanitize_to(fn: str, tl: dict[int, int]) -> str:
def sanitize_to(fn: str, tl: dict[int, int], fsnt: Optional[str] = None) -> str:
fn = fn.translate(tl)
if ANYWIN:
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += ("com%s lpt%s" % (n, n)).split(" ")
if fn.lower().split(".")[0] in bad:
if ANYWIN or fsnt == "win":
fn = fn.rstrip(". ")
if fn.lower().split(".")[0].strip() in BADNAMESET:
fn = "_" + fn
return fn
def sanitize_vpath(vp: str) -> str:
if not APTL_OS:
def sanitize_vpath(vp: str, fsnt: Optional[str] = None) -> str:
vptl = VPTL_OS
if fsnt and fsnt != "lin":
vptl = VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC
if not vptl:
return vp
parts = vp.replace(os.sep, "/").split("/")
ret = [sanitize_to(x, APTL_OS) for x in parts]
ret = [sanitize_to(x, vptl, fsnt) for x in parts]
return "/".join(ret)
@@ -4049,9 +4086,9 @@ def runihook(
}
for x, vp, ap in zip(ups, vps, aps)
]
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace")
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace") + b"\n"
else:
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps) + b"\n"
if acmd[0].startswith("zmq:"):
try:
+1
View File
@@ -705,6 +705,7 @@ window.baguetteBox = (function () {
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
timer.rm(rotn);
clearTimeout(unrot_timer);
}
function prepareOverlay(gallery, userOptions, cbz) {
+33 -13
View File
@@ -3559,7 +3559,7 @@ function eval_hash() {
if (hash0 && window.og_fn) {
var all = msel.getall(), mi;
for (var a = 0; a < all.length; a++)
if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {
if (og_fn == all[a].fn) {
mi = all[a];
break;
}
@@ -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'),
@@ -4254,9 +4254,6 @@ var fileman = (function () {
s2d[a] = all.indexOf(sel[a]);
var vp = sel[a].vp;
if (vp.endsWith('/'))
vp = vp.slice(0, -1);
var vsp = vsplit(vp);
if (base != vsp[0])
return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0]));
@@ -4575,6 +4572,8 @@ var fileman = (function () {
xhr.onload = xhr.onerror = rename_cb;
xhr.send();
}
ire.oninput();
};
r.delete = function (e) {
@@ -4601,6 +4600,9 @@ var fileman = (function () {
if (err !== 'xbd')
toast.ok(2, L.fd_ok);
if (QS('#seldoc.sel'))
thegrid.setvis(true);
treectl.goto();
return;
}
@@ -5077,11 +5079,22 @@ var showfile = (function () {
r.nmap = {
'dockerfile': 'docker'
};
r.pmap = {
'sh': 'bash',
'ash': 'bash',
'dash': 'bash',
'luajit': 'lua',
'node': 'js',
};
var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig';
x = x.split(/ +/g);
for (var a = 0; a < x.length; a++)
if (!r.map["." + x[a]])
r.map["." + x[a]] = x[a];
x = 'awk bash lua make perl python ruby swift';
x = x.split(/ +/g);
for (var a = 0; a < x.length; a++)
r.pmap[x[a]] = x[a];
r.sname = function (srch) {
return srch.split(/[?&]doc=/)[1].split('&')[0];
@@ -5090,7 +5103,7 @@ var showfile = (function () {
if (window.og_fn) {
var ext = og_fn.split(/\./g).pop();
if (r.map['.' + ext])
hist_replace(get_evpath() + '?doc=' + og_fn);
hist_replace(get_evpath() + '?doc=' + uricom_enc(og_fn));
}
window.Prism = { 'manual': true };
@@ -5101,7 +5114,7 @@ var showfile = (function () {
var m = /[?&]doc=([^&]+)/.exec(location.search);
if (m) {
setTimeout(function () {
r.show(uricom_dec(m[1]), true);
r.show(m[1], true);
}, 1);
}
}
@@ -5121,11 +5134,17 @@ var showfile = (function () {
return !!/[?&]doc=/.exec(location.search);
};
r.getlang = function (fn) {
r.getlang = function (fn, txt) {
fn = fn.toLowerCase();
var ext = fn.slice(fn.lastIndexOf('.'));
return r.map[ext] || r.nmap[fn];
}
var prog = txt && txt.startsWith('#!') && txt.split('\n')[0].replace(
'#!/usr/bin/env', '').replace(
'#!/usr/bin/', '').replace(
'#!/bin/', '').trim().split(' ')[0];
if (prog)
prog = r.pmap[prog] || r.pmap[prog.replace(/[0-9].*/, '')];
return r.map[ext] || r.nmap[fn] || prog;
};
r.addlinks = function () {
r.files = [];
@@ -5138,7 +5157,7 @@ var showfile = (function () {
if (!lang)
continue;
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
r.files.push({ 'id': link.id, 'name': link.fn });
var ah = ebi(link.id),
td = ah.closest('tr').getElementsByTagName('td')[0];
@@ -5274,7 +5293,7 @@ var showfile = (function () {
txt = doc[2],
name = url.split('?')[0].split('/').pop(),
tname = uricom_dec(name),
lang = r.getlang(name),
lang = r.getlang(name, txt),
is_md = lang == 'md';
ebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none';
@@ -5463,7 +5482,7 @@ var showfile = (function () {
sel = false;
for (var b = 0; b < sels.length; b++)
if (vsplit(sels[b].vp)[1] == lin)
if (sels[b].fn == lin)
sel = true;
clmod(lis[a], 'hl', lin == fn);
@@ -8768,6 +8787,7 @@ var msel = (function () {
item.id = links[a].getAttribute('id');
item.sel = clgot(links[a].closest('tr'), 'sel');
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
item.fn = uricom_dec(href.split('/').pop());
if (dk) {
var m = /[?&](k=[^&#]+)/.exec(qhref);
+15 -10
View File
@@ -1012,6 +1012,11 @@ function up2k_init(subtle) {
return true;
}
function unxhr(xhr) {
xhr.onload = xhr.onerror = xhr.ontimeout = null;
apop(st.xhr, xhr);
}
var pvis = new U2pvis("bz", '#u2cards', uc, st),
donut = new Donut(uc, st);
@@ -1934,10 +1939,8 @@ function up2k_init(subtle) {
if (!st.deaf)
console.log('xhr: bad apple');
xhr.onerror = xhr.ontimeout = null;
xhr.onload();
xhr.abort();
st.deaf = true;
xhr.onload();
}
if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) {
@@ -2498,12 +2501,12 @@ function up2k_init(subtle) {
if (!toast.visible)
toast.warn(9.98, L.u_enethd + "\n\nfile: " + esc(t.name), t);
apop(st.xhr, xhr);
unxhr(xhr);
apop(st.busy.head, t);
st.todo.head.unshift(t);
};
function orz(e) {
apop(st.xhr, xhr);
unxhr(xhr);
if (t.done)
return console.log('done; skip head2', t.name, t);
@@ -2571,7 +2574,7 @@ function up2k_init(subtle) {
var xhr = new XHR();
xhr.onerror = xhr.ontimeout = function () {
apop(st.xhr, xhr);
unxhr(xhr);
if (t.t_busied != me) // t.done ok
return console.log('zombie handshake onerror', t.name, t);
@@ -2584,7 +2587,7 @@ function up2k_init(subtle) {
t.keepalive = keepalive;
};
var orz = function (e) {
apop(st.xhr, xhr);
unxhr(xhr);
if (t.t_busied != me || t.done)
return console.log('zombie handshake onload', t.name, t);
@@ -2963,7 +2966,7 @@ function up2k_init(subtle) {
pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr);
var orz = function (xhr) {
apop(st.xhr, xhr);
unxhr(xhr);
st.bytes.inflight -= xhr.bsent;
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
if (txt.indexOf('upload blocked by x') + 1) {
@@ -3005,7 +3008,7 @@ function up2k_init(subtle) {
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
if (txt.indexOf('unknown wark') == 0) {
if (!t.sprs || txt.indexOf('unknown wark') == 0) {
t.cooldown = t.coolmul = 0;
unqueue_up(t);
}
@@ -3014,7 +3017,9 @@ function up2k_init(subtle) {
orz2(xhr);
}
var orz2 = function (xhr) {
apop(st.xhr, xhr);
unxhr(xhr);
if (st.deaf)
xhr.abort();
apop(st.busy.upload, upt);
for (var a = pcar; a <= pcdr; a++)
apop(t.postlist, a);
+2 -2
View File
@@ -853,7 +853,7 @@ function vjoin(p1, p2) {
if (!p2)
p2 = '';
if (p1.endsWith('/'))
if (p1 != '/' && p1.endsWith('/'))
p1 = p1.slice(0, -1);
if (p2.startsWith('/'))
@@ -865,7 +865,7 @@ function vjoin(p1, p2) {
if (!p2)
return p1;
return p1 + '/' + p2;
return p1 == '/' ? p1 + p2 : p1 + '/' + p2;
}
+84
View File
@@ -1,3 +1,87 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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
## 🩹 bugfixes
* the custom right-click-menu didn't like being enabled ad9994be
* this release is a hotfix for that; see v1.20.22 for all the other new stuff
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0906-2112 `v1.20.22` hello fedora
+1 -1
View File
@@ -49,7 +49,7 @@ echo warming up cache
cat 1 >/dev/null
echo ok lets go
$pybin "$sfx" -p39204 -e2dsa --dbd=yolo --exit=idx -lo=t -q "$@" && err= || err=$?
$pybin "$sfx" -i no -e2dsa --dbd=yolo --exit=idx -lo=t -q "$@" && err= || err=$?
[ $win ] && [ $err = 15 ] && err= # sigterm doesn't hook on windows, ah whatever
[ $err ] && echo ERROR $err && exit $err
+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.6 \
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
+1 -1
View File
@@ -15,7 +15,7 @@ command -v gsort >/dev/null && {
cd ../dist
kwds='-bind -accounts -auth -auth-ord -flags -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
kwds='-bind -accounts -auth -auth-ord -flags -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
export PRTY_FULL_HELP=1
export PRTY_NO_PARAMIKO=1
+1 -1
View File
@@ -8,7 +8,7 @@ import subprocess as sp
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r""""
echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
echo; for a in '' -bind -accounts -auth -auth-ord -flags -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
"""
# click [edit] => [select all]
+1 -1
View File
@@ -29,7 +29,7 @@ exit 0
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
for a in '' -bind -accounts -auth -auth-ord -flags -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c
+1 -1
View File
@@ -31,5 +31,5 @@ e9492eedaac7cfddde6d84389ef7df86c68275e8f29d838fc431d9bf379401bf6d82ec70ac024619
8ea9946d8484a6de833a97d3cd545c2b67475207944856281cffb2c513ab14bf50e2b9d31d7cca8f50a6ca8b6711fb8e45cbadabd566cab28f7d58ee058dc650 pillow-12.3.0-cp314-cp314-win_amd64.whl
6f7ed5dc47ade9ad56f6f0499bc5d8eb485bd454d47189133297fa5aa633cf70dad18656cd673318c664f96097c39003d94d9c9703c8337d62840b4564695a04 pyinstaller-6.21.0-py3-none-win_amd64.whl
0a7c1ae7063a2ee46a1c5c057fe73838d7dc5c59800e56037b3fb9dbd1c363915375f989160e44f21436e1071713682bb14473aa8112128beb670f5b110dc8e2 pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
a4adbefaa216571ade7fda561ed3404c762ff03029696ce1745fd4e23b066cb49b1fef99792ce3a594a04f9260a2f05a9e2f851bcc83ed4404f00ce05709c56a python-3.14.7-amd64.exe
ccbbc7ed972a8ae3f9ace7728cf5cbff8dcb15ed2dfcd105131641f5b041cecc90adc2fc25f36333e0a2be8e56a6f702fb064da6e6fbb78cc2393333ad3e5b6f python-3.14.8-amd64.exe
bfda5996cdf908edf971df38421ed154e2f2c58f38731ad3be22a88e78c933c64efd35fbc6daf82845dc0b59b5211e925038b430749b2c851887096578c68219 setuptools-83.0.0-py3-none-any.whl
+1 -1
View File
@@ -42,7 +42,7 @@ fns=(
pillow-12.3.0-cp314-cp314-win_amd64.whl
pyinstaller-6.21.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
python-3.14.7-amd64.exe
python-3.14.8-amd64.exe
setuptools-83.0.0-py3-none-any.whl
)
[ $w7 ] && fns+=(
+3
View File
@@ -70,6 +70,9 @@ done
[ "$1" ] || {
python3 ../scripts/test/smoketest.py &
pids+=($!)
bash ../scripts/test/redup.sh &
pids+=($!)
}
for pid in ${pids[@]}; do
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -e
td=redump-test
export TZ=Etc/UTC
rl_always=--reflink=always
rl_never=--reflink=never
[ "$(uname)" = Darwin ] && {
rl_always=
rl_never=
stat() { gstat "$@"; }
}
st_orig() { cat <<'EOF'
f1,1,'f1',3660
f2,1,'f2',3720
f3,2,'f3',3960
f4,1,'f4',3840
f5,1,'f5' -> 'f2',3900
f6,2,'f6',3960
f7,1,'f7',4020
f8,1,'f8',4080
f9,1,'f9',4140
EOF
}
st_ref() { cat <<'EOF'
f1,1,'f1',3660
f2,1,'f2',3720
f3,1,'f3',3960
f4,1,'f4',3840
f5,1,'f5',3900
f6,1,'f6',3960
f7,1,'f7',4020
f8,1,'f8',4080
f9,1,'f9',4140
EOF
}
st_sym() { cat <<'EOF'
f1,1,'f1',3660
f2,1,'f2',3720
f3,1,'f3',3960
f4,1,'f4',3840
f5,1,'f5' -> 'f2',3900
f6,1,'f6' -> 'f3',3960
f7,1,'f7' -> 'f4',4020
f8,1,'f8' -> 'f1',4080
f9,1,'f9' -> 'f1',4140
EOF
}
st_hard() { cat <<'EOF'
f1,3,'f1',3600
f2,2,'f2',3600
f3,2,'f3',3600
f4,2,'f4',3600
f5,2,'f5',3600
f6,2,'f6',3600
f7,2,'f7',3600
f8,3,'f8',3600
f9,3,'f9',3600
EOF
}
statchk() {
(cd $td;stat -c%n,%h,%N,%Y * | sort -n | tr -d \' | diff -U9 <($1|tr -d \') -) && return
cat $td/* | grep -vE '^f1f2f3f4f2f3f4f1f1$' || return
cat $td.l
exit 1
}
setup() {
rm -rf $td
mkdir $td
th=19700101010 #..M
( cd "$td"
for n in {1..4}; do echo -n f$n > f$n; done
ln -s f2 f5
ln f3 f6
cp $rl_always f4 f7
cp $rl_never f1 f8
cp $rl_never f1 f9
for n in {1..9}; do touch -ht $th$n f$n; done
);statchk st_orig
}
export PRTY_NO_ARGON2=1
export PRTY_NO_CFSSL=1
export PRTY_NO_FFMPEG=1
export PRTY_NO_FFPROBE=1
export PRTY_NO_IFADDR=1
export PRTY_NO_IMPRESO=1
export PRTY_NO_MAGIC=1
export PRTY_NO_PARAMIKO=1
export PRTY_NO_PARTFTPY=1
export PRTY_NO_PIL=1
export PRTY_NO_PSUTIL=1
export PRTY_NO_PYFTPD=1
export PRTY_NO_RAW=1
export PRTY_NO_VIPS=1
cmd="python -m copyparty -i no --ign-ebind-all --no-ses --no-fastboot --no-voldump --exit=idx --ansi -v $td::A"
fs=$(stat -fc%T .)
if [ "$fs" = btrfs ]; then
echo ref:; setup; $cmd -e2dsa --redup sym,hard,ref=ref >$td.l; statchk st_ref
else
printf 'detected filesystem [%s] which is not btrfs, will NOT run reflink test\n' "$fs"
fi
echo sym:; setup; $cmd -e2dsa --redup sym,hard,ref=sym >$td.l; statchk st_sym
echo hard:; setup; $cmd -e2dsa --redup sym,hard,ref=hard >$td.l; touch -ht 197001010100 $td/*; statchk st_hard
# `- ign hard mtimes because redup is order-undefined (or sqlite-row-order rather but ye)
rm -rf $td $td.l
+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]))
+6 -5
View File
@@ -145,10 +145,10 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "allow_flac allow_wav allow_svg_js chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mime no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss show_hist smb srch_dbg srch_excl srch_nfkc srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme wopi write_uplog xdev xlink xvol zipmaxu zs"
ex = "allow_flac allow_wav allow_svg_js chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mime no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz redup_dry reflink rm_partial rmagic rss show_hist smb srch_dbg srch_excl srch_nfkc srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme wopi write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hist_cow hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ex = "dav_inf dedup dotpart dotsrch hack_me_bro hist_cow hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other lf_url mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
@@ -163,16 +163,16 @@ 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 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 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"
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"
ka.update(**{k: "" for k in ex.split()})
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",