mirror of
https://github.com/9001/copyparty.git
synced 2026-11-10 12:38:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ccebb6948 | ||
|
|
ed0fcd608f | ||
|
|
9811cd3101 | ||
|
|
fa8913c712 | ||
|
|
4bd64d7456 | ||
|
|
7ce483e970 | ||
|
|
82eab3d2f8 | ||
|
|
0dc1cb3291 | ||
|
|
1a87dc3218 | ||
|
|
dc3adc0f4d | ||
|
|
5f00c1631c | ||
|
|
e59fc05e73 | ||
|
|
e8de421174 | ||
|
|
36aaa24974 | ||
|
|
5e0a88e355 | ||
|
|
4b283b4bfd | ||
|
|
64f574fe94 | ||
|
|
a31870cbd3 | ||
|
|
74b17374c2 | ||
|
|
670e86228c | ||
|
|
1341b3421f | ||
|
|
5961228c93 | ||
|
|
28f9f6f002 | ||
|
|
186cbf939f | ||
|
|
080935788b | ||
|
|
c6918c2ce6 | ||
|
|
ab2299e3db | ||
|
|
334dccc55e | ||
|
|
8d7aa4f860 | ||
|
|
f73b37c6fa | ||
|
|
731dbbff84 | ||
|
|
23e00c0a04 | ||
|
|
107dec9ed1 | ||
|
|
f445bfa862 | ||
|
|
aff083fb28 | ||
|
|
5ccc999f39 | ||
|
|
16a3f5d51a | ||
|
|
35af7c9fef | ||
|
|
4d1f2a9301 | ||
|
|
229159027b | ||
|
|
ec4a95e1a5 | ||
|
|
eb4f1dddce | ||
|
|
097b83056e | ||
|
|
c688bb2e46 | ||
|
|
c92435c2d2 | ||
|
|
22afaa7586 | ||
|
|
5ddbce31fc | ||
|
|
ec8ec0f1a6 | ||
|
|
558f6aee44 | ||
|
|
6867ccc511 | ||
|
|
947d73eeb6 | ||
|
|
735d6e00de | ||
|
|
fd4f434192 | ||
|
|
971a62cc89 | ||
|
|
bfc3cff848 | ||
|
|
8967726f61 |
No files matched your search
@@ -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
@@ -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<&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:
|
||||
|
||||
@@ -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.25"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -26,7 +26,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/copyparty.conf")
|
||||
install=copyparty.install
|
||||
sha256sums=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
|
||||
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.22
|
||||
pkgver=1.20.25
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("/etc/${pkgname}.d/init" )
|
||||
sha256sums=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
|
||||
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -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.25/copyparty-1.20.25.tar.gz",
|
||||
"version": "1.20.25",
|
||||
"hash": "sha256-gsaN+kLgill1e7+gsdFAAyFBF1NHkG4Fa++DkE9cLHA="
|
||||
}
|
||||
+88
-13
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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+=(
|
||||
|
||||
@@ -70,6 +70,9 @@ done
|
||||
[ "$1" ] || {
|
||||
python3 ../scripts/test/smoketest.py &
|
||||
pids+=($!)
|
||||
|
||||
bash ../scripts/test/redup.sh &
|
||||
pids+=($!)
|
||||
}
|
||||
|
||||
for pid in ${pids[@]}; do
|
||||
|
||||
Executable
+112
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user