From d636316a19ecf6ba269b0b5d7e4358ccba7cdf8e Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 10 Feb 2024 18:37:21 +0000 Subject: [PATCH 01/36] add tftp server --- README.md | 27 +++- contrib/package/arch/PKGBUILD | 2 +- copyparty/__main__.py | 15 ++- copyparty/svchub.py | 28 +++- copyparty/tcpsrv.py | 1 + copyparty/tftpd.py | 241 ++++++++++++++++++++++++++++++++++ copyparty/util.py | 15 ++- docs/devnotes.md | 1 + docs/lics.txt | 4 + docs/versus.md | 7 +- pyproject.toml | 1 + scripts/make-sfx.sh | 28 +++- scripts/sfx.ls | 1 + setup.py | 3 +- 14 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 copyparty/tftpd.py diff --git a/README.md b/README.md index 4689005b..37c1bb94 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser * server only needs Python (2 or 3), all dependencies optional -* ๐Ÿ”Œ protocols: [http](#the-browser) // [ftp](#ftp-server) // [webdav](#webdav-server) // [smb/cifs](#smb-server) +* ๐Ÿ”Œ protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server) * ๐Ÿ“ฑ [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts) ๐Ÿ‘‰ **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** ๐Ÿ‘€ running from a basement in finland @@ -53,6 +53,7 @@ turn almost any device into a file server with resumable uploads/downloads using * [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921` * [webdav server](#webdav-server) - with read-write support * [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI + * [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969` * [smb server](#smb-server) - unsafe, slow, not recommended for wan * [browser ux](#browser-ux) - tweaking the ui * [file indexing](#file-indexing) - enables dedup and music search ++ @@ -157,11 +158,11 @@ you may also want these, especially on servers: and remember to open the ports you want; here's a complete example including every feature copyparty has to offer: ``` firewall-cmd --permanent --add-port={80,443,3921,3923,3945,3990}/tcp # --zone=libvirt -firewall-cmd --permanent --add-port=12000-12099/tcp --permanent # --zone=libvirt -firewall-cmd --permanent --add-port={1900,5353}/udp # --zone=libvirt +firewall-cmd --permanent --add-port=12000-12099/tcp # --zone=libvirt +firewall-cmd --permanent --add-port={69,1900,3969,5353}/udp # --zone=libvirt firewall-cmd --reload ``` -(1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3990:ftps, 5353:mdns, 12000:passive-ftp) +(69:tftp, 1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3969:tftp, 3990:ftps, 5353:mdns, 12000:passive-ftp) ## features @@ -172,6 +173,7 @@ firewall-cmd --reload * โ˜‘ volumes (mountpoints) * โ˜‘ [accounts](#accounts-and-volumes) * โ˜‘ [ftp server](#ftp-server) + * โ˜‘ [tftp server](#tftp-server) * โ˜‘ [webdav server](#webdav-server) * โ˜‘ [smb/cifs server](#smb-server) * โ˜‘ [qr-code](#qr-code) for quick access @@ -943,6 +945,23 @@ known client bugs: * latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp) +## tftp server + +a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 80s (in which case we should definitely hang some time)) + +* based on [partftpy](https://github.com/9001/partftpy) +* needs a dedicated port (cannot share with the HTTP/HTTPS API) + * run as root to use the spec-recommended port `69` (nice) +* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable +* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported (will be extremely slow over WAN) + +some recommended TFTP clients: +* windows: `tftp.exe` (you probably already have it) +* linux: `tftp-hpa`, `atftp` + * `tftp 127.0.0.1 3969 -v -m binary -c put initrd.bin` +* `curl` (read-only) + + ## smb server unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 98c8854a..34c86870 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty pkgver="1.9.31" pkgrel=1 -pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, zeroconf, media indexer, thumbnails++" +pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") url="https://github.com/9001/${pkgname}" license=('MIT') diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f7161235..ef1540f9 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -46,6 +46,7 @@ from .util import ( PY_DESC, PYFTPD_VER, SQLITE_VER, + PARTFTPY_VER, UNPLICATIONS, align_tab, ansi_re, @@ -993,7 +994,7 @@ def add_zc_ssdp(ap): def add_ftp(ap): - ap2 = ap.add_argument_group('FTP options') + ap2 = ap.add_argument_group('FTP options (TCP only)') ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921") ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990") ap2.add_argument("--ftpv", action="store_true", help="verbose") @@ -1013,6 +1014,14 @@ def add_webdav(ap): ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)") +def add_tftp(ap): + ap2 = ap.add_argument_group('TFTP options (UDP only)') + ap2.add_argument("--tftp", metavar="PORT", type=int, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") + ap2.add_argument("--tftpv", action="store_true", help="verbose") + ap2.add_argument("--tftpvv", action="store_true", help="verboser") + ap2.add_argument("--tftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]") + + def add_smb(ap): ap2 = ap.add_argument_group('SMB/CIFS options') ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!") @@ -1322,6 +1331,7 @@ def run_argparse( add_transcoding(ap) add_ftp(ap) add_webdav(ap) + add_tftp(ap) add_smb(ap) add_safety(ap) add_salt(ap, fk_salt, ah_salt) @@ -1375,7 +1385,7 @@ def main(argv: Optional[list[str]] = None) -> None: if argv is None: argv = sys.argv - f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite v{} | jinja2 v{} | pyftpd v{}\n\033[0m' + f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite {} | jinja {} | pyftpd {} | tftp {}\n\033[0m' f = f.format( S_VERSION, CODENAME, @@ -1384,6 +1394,7 @@ def main(argv: Optional[list[str]] = None) -> None: SQLITE_VER, JINJA_VER, PYFTPD_VER, + PARTFTPY_VER, ) lprint(f) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 65d87d53..49f49068 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -133,7 +133,7 @@ class SvcHub(object): if not self._process_config(): raise Exception(BAD_CFG) - # for non-http clients (ftp) + # for non-http clients (ftp, tftp) self.bans: dict[str, int] = {} self.gpwd = Garda(self.args.ban_pw) self.g404 = Garda(self.args.ban_404) @@ -268,6 +268,12 @@ class SvcHub(object): Daemon(self.start_ftpd, "start_ftpd") zms += "f" if args.ftp else "F" + if args.tftp: + from .tftpd import Tftpd + + self.tftpd: Optional[Tftpd] = None + Daemon(self.start_ftpd, "start_tftpd") + if args.smb: # impacket.dcerpc is noisy about listen timeouts sto = socket.getdefaulttimeout() @@ -297,10 +303,12 @@ class SvcHub(object): def start_ftpd(self) -> None: time.sleep(30) - if self.ftpd: - return - self.restart_ftpd() + if hasattr(self, "ftpd") and not self.ftpd: + self.restart_ftpd() + + if hasattr(self, "tftpd") and not self.tftpd: + self.restart_tftpd() def restart_ftpd(self) -> None: if not hasattr(self, "ftpd"): @@ -317,6 +325,17 @@ class SvcHub(object): self.ftpd = Ftpd(self) self.log("root", "started FTPd") + def restart_tftpd(self) -> None: + if not hasattr(self, "tftpd"): + return + + from .tftpd import Tftpd + + if self.tftpd: + return # todo + + self.tftpd = Tftpd(self) + def thr_httpsrv_up(self) -> None: time.sleep(1 if self.args.ign_ebind_all else 5) expected = self.broker.num_workers * self.tcpsrv.nsrv @@ -444,6 +463,7 @@ class SvcHub(object): al.xff_re = self._ipa2re(al.xff_src) al.ipa_re = self._ipa2re(al.ipa) al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or al.ipa) + al.tftp_ipa_re = self._ipa2re(al.tftp_ipa or al.ipa) mte = ODict.fromkeys(DEF_MTE.split(","), True) al.mte = odfusion(mte, al.mte) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 06ee6f4a..4bbea2c9 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -309,6 +309,7 @@ class TcpSrv(object): self.hub.start_zeroconf() gencert(self.log, self.args, self.netdevs) self.hub.restart_ftpd() + self.hub.restart_tftpd() def shutdown(self) -> None: self.stopping = True diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py new file mode 100644 index 00000000..9155186f --- /dev/null +++ b/copyparty/tftpd.py @@ -0,0 +1,241 @@ +# coding: utf-8 +from __future__ import print_function, unicode_literals + +try: + from types import SimpleNamespace +except: + class SimpleNamespace(object): + def __init__(self, **attr): + self.__dict__.update(attr) + +import inspect +import logging +import os +import stat + +from partftpy import TftpContexts, TftpServer, TftpStates +from partftpy.TftpShared import TftpException + +from .__init__ import PY2, TYPE_CHECKING +from .authsrv import VFS +from .bos import bos +from .util import Daemon, min_ex, pybin, runhook, undot + +if True: # pylint: disable=using-constant-test + from typing import Any, Union + +if TYPE_CHECKING: + from .svchub import SvcHub + + +lg = logging.getLogger("tftp") +debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error) + + +def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool: + info("connection from %s:%s", raddress, rport) + ret = _orig_serverInitial(self, pkt, raddress, rport) + ptn = _hub[0].args.tftp_ipa_re + if ptn and not ptn.match(raddress): + yeet("client rejected (--tftp-ipa): %s" % (raddress,)) + return ret + +# patch ipa-check into partftpd +_hub: list["SvcHub"] = [] +_orig_serverInitial = TftpStates.TftpServerState.serverInitial +TftpStates.TftpServerState.serverInitial = _serverInitial + + +class Tftpd(object): + def __init__(self, hub: "SvcHub") -> None: + self.hub = hub + self.args = hub.args + self.asrv = hub.asrv + self.log = hub.log + + _hub.clear() + _hub.append(hub) + + lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) + for x in ["partftpy", "partftpy.TftpStates", "partftpy.TftpServer"]: + lgr = logging.getLogger(x) + lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) + + # patch vfs into partftpy + TftpContexts.open = self._open + TftpStates.open = self._open + + fos = SimpleNamespace() + for k in os.__dict__: + try: + setattr(fos, k, getattr(os, k)) + except: + pass + fos.access = self._access + fos.mkdir = self._mkdir + fos.unlink = self._unlink + fos.sep = "/" + TftpContexts.os = fos + TftpServer.os = fos + TftpStates.os = fos + + fop = SimpleNamespace() + for k in os.path.__dict__: + try: + setattr(fop, k, getattr(os.path, k)) + except: + pass + fop.abspath = self._p_abspath + fop.exists = self._p_exists + fop.isdir = self._p_isdir + fop.normpath = self._p_normpath + fos.path = fop + + self._disarm(fos) + + ip = next((x for x in self.args.i if ":" not in x), None) + if not ip: + self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3) + ip = "0.0.0.0" + + self.ip = ip + self.port = int(self.args.tftp) + self.srv = TftpServer.TftpServer("/", self._ls) + self.stop = self.srv.stop + + Daemon(self.srv.listen, "tftp", [self.ip, self.port]) + + # XXX TODO hook TftpContextServer.start; + # append tftp-ipa check at bottom and throw TftpException if not match + + def nlog(self, msg: str, c: Union[int, str] = 0) -> None: + self.log("tftp", msg, c) + + def _v2a( + self, caller: str, vpath: str, perms: list, *a: Any + ) -> tuple[VFS, str]: + vpath = vpath.replace("\\", "/").lstrip("/") + if not perms: + perms = [True, True] + + debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms) + vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) + return vfs, vfs.canonical(rem) + + def _ls(self, vpath: str) -> Any: + # generate file listing if vpath is dir.txt and return as file object + return None + + def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any: + rd = wr = False + if mode == "rb": + rd = True + elif mode == "wb": + wr = True + else: + raise Exception("bad mode %s" % (mode,)) + + vfs, ap = self._v2a("open", vpath, [rd, wr]) + if wr: + if "*" not in vfs.axs.uwrite: + yeet("blocked write; folder not world-writable: /%s" % (vpath,)) + + if bos.path.exists(ap) and "*" not in vfs.axs.udel: + yeet("blocked write; folder not world-deletable: /%s" % (vpath,)) + + xbu = vfs.flags.get("xbu") + if xbu and not runhook( + self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, "" + ): + yeet("blocked by xbu server config: " + vpath) + + return open(ap, mode, *a, **ka) + + def _mkdir(self, vpath: str, *a) -> None: + vfs, ap = self._v2a("mkdir", vpath, []) + if "*" not in vfs.axs.uwrite: + yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) + + return bos.mkdir(ap) + + def _unlink(self, vpath: str) -> None: + # return bos.unlink(self._v2a("stat", vpath, *a)[1]) + vfs, ap = self._v2a( + "delete", vpath, [True, False, False, True] + ) + + try: + inf = bos.stat(ap) + except: + return + + if not stat.S_ISREG(inf.st_mode) or inf.st_size: + yeet("attempted delete of non-empty file") + + vpath = vpath.replace("\\", "/").lstrip("/") + self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False) + + def _access(self, *a: Any) -> bool: + return True + + def _p_abspath(self, vpath: str) -> str: + return "/" + undot(vpath) + + def _p_normpath(self, *a: Any) -> str: + return "" + + def _p_exists(self, vpath: str) -> bool: + try: + ap = self._v2a("p.exists", vpath, [False, False])[1] + bos.stat(ap) + return True + except: + return False + + def _p_isdir(self, vpath: str) -> bool: + try: + st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1]) + ret = stat.S_ISDIR(st.st_mode) + return ret + except: + return False + + def _hook(self, *a: Any, **ka: Any) -> None: + src = inspect.currentframe().f_back.f_code.co_name + error("\033[31m%s:hook(%s)\033[0m", src, a) + raise Exception("nope") + + def _disarm(self, fos: SimpleNamespace) -> None: + fos.chmod = self._hook + fos.chown = self._hook + fos.close = self._hook + fos.ftruncate = self._hook + fos.lchown = self._hook + fos.link = self._hook + fos.listdir = self._hook + fos.lstat = self._hook + fos.open = self._hook + fos.remove = self._hook + fos.rename = self._hook + fos.replace = self._hook + fos.scandir = self._hook + fos.stat = self._hook + fos.symlink = self._hook + fos.truncate = self._hook + fos.utime = self._hook + fos.walk = self._hook + + fos.path.expanduser = self._hook + fos.path.expandvars = self._hook + fos.path.getatime = self._hook + fos.path.getctime = self._hook + fos.path.getmtime = self._hook + fos.path.getsize = self._hook + fos.path.isabs = self._hook + fos.path.isfile = self._hook + fos.path.islink = self._hook + fos.path.realpath = self._hook + +def yeet(msg: str) -> None: + warning(msg) + raise TftpException(msg) diff --git a/copyparty/util.py b/copyparty/util.py index 04814517..e4295f34 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -423,16 +423,21 @@ try: except: PYFTPD_VER = "(None)" +try: + from partftpy.__init__ import __version__ as PARTFTPY_VER +except: + PARTFTPY_VER = "(None)" + PY_DESC = py_desc() -VERSIONS = "copyparty v{} ({})\n{}\n sqlite v{} | jinja v{} | pyftpd v{}".format( - S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER +VERSIONS = "copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {}".format( + S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER ) -_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER) -__all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER"] +_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER) +__all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER", "PARTFTPY_VER"] class Daemon(threading.Thread): @@ -536,6 +541,8 @@ class HLog(logging.Handler): elif record.name.startswith("impacket"): if self.ptn_smb_ign.match(msg): return + elif record.name.startswith("partftpy."): + record.name = record.name[9:] self.log_func(record.name[-21:], msg, c) diff --git a/docs/devnotes.md b/docs/devnotes.md index 10ac2a86..49023b81 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -242,6 +242,7 @@ python3 -m venv .venv pip install jinja2 strip_hints # MANDATORY pip install mutagen # audio metadata pip install pyftpdlib # ftp server +pip install partftpy # tftp server pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails pip install pyvips # faster thumbnails diff --git a/docs/lics.txt b/docs/lics.txt index bc155584..6f403418 100644 --- a/docs/lics.txt +++ b/docs/lics.txt @@ -24,6 +24,10 @@ https://github.com/giampaolo/pyftpdlib/ C: 2007 Giampaolo Rodola L: MIT +https://github.com/9001/partftpy +C: 2010-2021 Michael P. Soulier +L: MIT + https://github.com/nayuki/QR-Code-generator/ C: Project Nayuki L: MIT diff --git a/docs/versus.md b/docs/versus.md index 50e802a5..02df6ed4 100644 --- a/docs/versus.md +++ b/docs/versus.md @@ -200,9 +200,10 @@ symbol legend, | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | | serve https | โ–ˆ | | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | | serve webdav | โ–ˆ | | | โ–ˆ | โ–ˆ | โ–ˆ | โ–ˆ | | โ–ˆ | | | โ–ˆ | -| serve ftp | โ–ˆ | | | | | โ–ˆ | | | | | | โ–ˆ | -| serve ftps | โ–ˆ | | | | | โ–ˆ | | | | | | โ–ˆ | -| serve sftp | | | | | | โ–ˆ | | | | | | โ–ˆ | +| serve ftp (tcp) | โ–ˆ | | | | | โ–ˆ | | | | | | โ–ˆ | +| serve ftps (tls) | โ–ˆ | | | | | โ–ˆ | | | | | | โ–ˆ | +| serve tftp (udp) | โ–ˆ | | | | | | | | | | | | +| serve sftp (ssh) | | | | | | โ–ˆ | | | | | | โ–ˆ | | serve smb/cifs | โ•ฑ | | | | | โ–ˆ | | | | | | | | serve dlna | | | | | | โ–ˆ | | | | | | | | listen on unix-socket | | | | โ–ˆ | โ–ˆ | | โ–ˆ | โ–ˆ | โ–ˆ | | โ–ˆ | โ–ˆ | diff --git a/pyproject.toml b/pyproject.toml index 8c1a1020..f940c2dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ thumbnails2 = ["pyvips"] audiotags = ["mutagen"] ftpd = ["pyftpdlib"] ftps = ["pyftpdlib", "pyopenssl"] +tftpd = ["partftpy"] pwhash = ["argon2-cffi"] [project.scripts] diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index c05dfa24..8a56ecab 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -26,8 +26,9 @@ help() { exec cat <<'EOF' # _____________________________________________________________________ # core features: # -# `no-ftp` saves ~33k by removing the ftp server and filetype detector, -# disabling --ftpd and --magic +# `no-ftp` saves ~30k by removing the ftp server, disabling --ftp +# +# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp # # `no-smb` saves ~3.5k by removing the smb / cifs server # @@ -114,6 +115,7 @@ while [ ! -z "$1" ]; do gz) use_gz=1 ; ;; gzz) shift;use_gzz=$1;use_gz=1; ;; no-ftp) no_ftp=1 ; ;; + no-tfp) no_tfp=1 ; ;; no-smb) no_smb=1 ; ;; no-zm) no_zm=1 ; ;; no-fnt) no_fnt=1 ; ;; @@ -165,7 +167,8 @@ necho() { [ $repack ] && { old="$tmpdir/pe-copyparty.$(id -u)" echo "repack of files in $old" - cp -pR "$old/"*{py2,py37,j2,copyparty} . + cp -pR "$old/"*{py2,py37,magic,j2,copyparty} . + cp -pR "$old/"*partftpy . || true cp -pR "$old/"*ftp . || true } @@ -221,6 +224,16 @@ necho() { mkdir ftp/ mv pyftpdlib ftp/ + necho collecting partftpy + f="../build/partftpy-0.1.0.tar.gz" + [ -e "$f" ] || + (url=https://files.pythonhosted.org/packages/55/25/e043193fb3d941b91fc84a55e0560b1c248f3f04d73747eb4f35f5e2776e/partftpy-0.1.0.tar.gz; + wget -O$f "$url" || curl -L "$url" >$f) + + tar -zxf $f + mv partftpy-*/partftpy . + rm -rf partftpy-* partftpy/bin + necho collecting python-magic v=0.4.27 f="../build/python-magic-$v.tar.gz" @@ -234,7 +247,6 @@ necho() { rm -rf python-magic-* rm magic/compat.py iawk '/^def _add_compat/{o=1} !o; /^_add_compat/{o=0}' magic/__init__.py - mv magic ftp/ # doesn't provide a version label anyways # enable this to dynamically remove type hints at startup, # in case a future python version can use them for performance @@ -409,8 +421,10 @@ iawk '/^ {0,4}[^ ]/{s=0}/^ {4}def (serve_forever|_loop)/{s=1}!s' ftp/pyftpdlib/s rm -f ftp/pyftpdlib/{__main__,prefork}.py [ $no_ftp ] && - rm -rf copyparty/ftpd.py ftp && - sed -ri '/\.ftp/d' copyparty/svchub.py + rm -rf copyparty/ftpd.py ftp + +[ $no_tfp ] && + rm -rf copyparty/tftpd.py partftpy [ $no_smb ] && rm -f copyparty/smbd.py @@ -584,7 +598,7 @@ nf=$(ls -1 "$zdir"/arc.* 2>/dev/null | wc -l) echo gen tarlist -for d in copyparty j2 py2 py37 ftp; do find $d -type f; done | # strip_hints +for d in copyparty partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done | # strip_hints sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort | sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1 diff --git a/scripts/sfx.ls b/scripts/sfx.ls index e21fec23..407137be 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -54,6 +54,7 @@ copyparty/sutil.py, copyparty/svchub.py, copyparty/szip.py, copyparty/tcpsrv.py, +copyparty/tftpd.py, copyparty/th_cli.py, copyparty/th_srv.py, copyparty/u2idx.py, diff --git a/setup.py b/setup.py index 2dd34184..9601970f 100755 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ args = { "version": about["__version__"], "description": ( "Portable file server with accelerated resumable uploads, " - + "deduplication, WebDAV, FTP, zeroconf, media indexer, " + + "deduplication, WebDAV, FTP, TFTP, zeroconf, media indexer, " + "video thumbnails, audio transcoding, and write-only folders" ), "long_description": long_description, @@ -140,6 +140,7 @@ args = { "audiotags": ["mutagen"], "ftpd": ["pyftpdlib"], "ftps": ["pyftpdlib", "pyopenssl"], + "tftpd": ["partftpy"], "pwhash": ["argon2-cffi"], }, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, From 8796c09f569872077a7a4c1831b65c4d0c668176 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 10 Feb 2024 21:45:57 +0000 Subject: [PATCH 02/36] add `--tftp-pr` to specify portrange instead of ephemerals --- copyparty/__main__.py | 1 + copyparty/tftpd.py | 10 ++++++---- pyproject.toml | 2 +- scripts/make-sfx.sh | 4 ++-- setup.py | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ef1540f9..3e190caf 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1020,6 +1020,7 @@ def add_tftp(ap): ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpvv", action="store_true", help="verboser") ap2.add_argument("--tftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]") + ap2.add_argument("--tftp-pr", metavar="P-P", type=u, help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") def add_smb(ap): diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 9155186f..ce23b626 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -103,10 +103,12 @@ class Tftpd(object): self.srv = TftpServer.TftpServer("/", self._ls) self.stop = self.srv.stop - Daemon(self.srv.listen, "tftp", [self.ip, self.port]) + ports = [] + if self.args.tftp_pr: + p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")] + ports = list(range(p1, p2 + 1)) - # XXX TODO hook TftpContextServer.start; - # append tftp-ipa check at bottom and throw TftpException if not match + Daemon(self.srv.listen, "tftp", [self.ip, self.port], ka={"ports": ports}) def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("tftp", msg, c) @@ -122,7 +124,7 @@ class Tftpd(object): vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) return vfs, vfs.canonical(rem) - def _ls(self, vpath: str) -> Any: + def _ls(self, vpath: str, raddress: str, rport: int) -> Any: # generate file listing if vpath is dir.txt and return as file object return None diff --git a/pyproject.toml b/pyproject.toml index f940c2dc..9e58d831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ thumbnails2 = ["pyvips"] audiotags = ["mutagen"] ftpd = ["pyftpdlib"] ftps = ["pyftpdlib", "pyopenssl"] -tftpd = ["partftpy"] +tftpd = ["partftpy>=0.2.0"] pwhash = ["argon2-cffi"] [project.scripts] diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index 8a56ecab..44f26938 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -225,9 +225,9 @@ necho() { mv pyftpdlib ftp/ necho collecting partftpy - f="../build/partftpy-0.1.0.tar.gz" + f="../build/partftpy-0.2.0.tar.gz" [ -e "$f" ] || - (url=https://files.pythonhosted.org/packages/55/25/e043193fb3d941b91fc84a55e0560b1c248f3f04d73747eb4f35f5e2776e/partftpy-0.1.0.tar.gz; + (url=https://files.pythonhosted.org/packages/64/4a/360dde1e7277758a4ccb0d6434ec661042d9d745aa6c3baa9ec0699df3e9/partftpy-0.2.0.tar.gz; wget -O$f "$url" || curl -L "$url" >$f) tar -zxf $f diff --git a/setup.py b/setup.py index 9601970f..4cc3cd2a 100755 --- a/setup.py +++ b/setup.py @@ -140,7 +140,7 @@ args = { "audiotags": ["mutagen"], "ftpd": ["pyftpdlib"], "ftps": ["pyftpdlib", "pyopenssl"], - "tftpd": ["partftpy"], + "tftpd": ["partftpy>=0.2.0"], "pwhash": ["argon2-cffi"], }, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, From acbb8267e1ab7eea7f62c467d6bfd227294127b0 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 10 Feb 2024 23:50:17 +0000 Subject: [PATCH 03/36] tftp: add directory listing --- copyparty/__main__.py | 4 ++- copyparty/svchub.py | 7 ++++ copyparty/tftpd.py | 84 ++++++++++++++++++++++++++++++++++++++----- copyparty/util.py | 17 +++++++-- tests/test_dots.py | 2 +- tests/util.py | 2 +- 6 files changed, 101 insertions(+), 15 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3e190caf..561f33c9 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -43,10 +43,10 @@ from .util import ( DEF_MTH, IMPLICATIONS, JINJA_VER, + PARTFTPY_VER, PY_DESC, PYFTPD_VER, SQLITE_VER, - PARTFTPY_VER, UNPLICATIONS, align_tab, ansi_re, @@ -1019,6 +1019,8 @@ def add_tftp(ap): ap2.add_argument("--tftp", metavar="PORT", type=int, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpvv", action="store_true", help="verboser") + ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") + ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") ap2.add_argument("--tftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]") ap2.add_argument("--tftp-pr", metavar="P-P", type=u, help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 49f49068..8bf3c645 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -451,6 +451,13 @@ class SvcHub(object): else: setattr(al, k, re.compile(vs)) + for k in "tftp_lsf".split(" "): + vs = getattr(al, k) + if not vs or vs == "no": + setattr(al, k, None) + else: + setattr(al, k, re.compile("^" + vs + "$")) + if not al.sus_urls: al.ban_url = "no" elif al.ban_url == "no": diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index ce23b626..8022b88a 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -4,14 +4,17 @@ from __future__ import print_function, unicode_literals try: from types import SimpleNamespace except: + class SimpleNamespace(object): def __init__(self, **attr): self.__dict__.update(attr) + import inspect import logging import os import stat +from datetime import datetime from partftpy import TftpContexts, TftpServer, TftpStates from partftpy.TftpShared import TftpException @@ -19,7 +22,7 @@ from partftpy.TftpShared import TftpException from .__init__ import PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos -from .util import Daemon, min_ex, pybin, runhook, undot +from .util import BytesIO, Daemon, exclude_dotfiles, runhook, undot if True: # pylint: disable=using-constant-test from typing import Any, Union @@ -40,6 +43,7 @@ def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool: yeet("client rejected (--tftp-ipa): %s" % (raddress,)) return ret + # patch ipa-check into partftpd _hub: list["SvcHub"] = [] _orig_serverInitial = TftpStates.TftpServerState.serverInitial @@ -113,9 +117,7 @@ class Tftpd(object): def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("tftp", msg, c) - def _v2a( - self, caller: str, vpath: str, perms: list, *a: Any - ) -> tuple[VFS, str]: + def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]: vpath = vpath.replace("\\", "/").lstrip("/") if not perms: perms = [True, True] @@ -124,9 +126,71 @@ class Tftpd(object): vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) return vfs, vfs.canonical(rem) - def _ls(self, vpath: str, raddress: str, rport: int) -> Any: + 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 - return None + if not force: + vpath, fn = os.path.split(vpath.replace("\\", "/")) + ptn = self.args.tftp_lsf + if not ptn or not ptn.match(fn.lower()): + return None + + vn, rem = self.asrv.vfs.get(vpath, "*", True, False) + fsroot, vfs_ls, vfs_virt = vn.ls( + rem, + "*", + not self.args.no_scandir, + [[True, False]], + ) + dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]) + dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames] + fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames] + real1 = dirs1 + fils1 + realt = [(datetime.fromtimestamp(mt), sz, fn) for mt, sz, fn in real1] + reals = [ + ( + "%04d-%02d-%02d %02d:%02d:%02d" + % ( + zd.year, + zd.month, + zd.day, + zd.hour, + zd.minute, + zd.second, + ), + sz, + fn, + ) + for zd, sz, fn in realt + ] + virs = [("????-??-?? ??:??:??", 0, k + "/") for k in vfs_virt.keys()] + ls = virs + reals + + if "*" not in vn.axs.udot: + names = set(exclude_dotfiles([x[2] for x in ls])) + ls = [x for x in ls if x[2] in names] + + try: + biggest = max([x[1] for x in ls]) + except: + biggest = 0 + + perms = [] + if "*" in vn.axs.uread: + perms.append("read") + if "*" in vn.axs.udot: + perms.append("hidden") + if "*" in vn.axs.uwrite: + if "*" in vn.axs.udel: + perms.append("overwrite") + else: + perms.append("write") + + fmt = "{{}} {{:{},}} {{}}" + fmt = fmt.format(len("{:,}".format(biggest))) + retl = ["# permissions: %s" % (", ".join(perms),)] + retl += [fmt.format(*x) for x in ls] + ret = "\n".join(retl).encode("utf-8", "replace") + return BytesIO(ret) def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any: rd = wr = False @@ -151,6 +215,9 @@ class Tftpd(object): ): yeet("blocked by xbu server config: " + vpath) + if not self.args.tftp_nols and bos.path.isdir(ap): + return self._ls(vpath, "", 0, True) + return open(ap, mode, *a, **ka) def _mkdir(self, vpath: str, *a) -> None: @@ -162,9 +229,7 @@ class Tftpd(object): def _unlink(self, vpath: str) -> None: # return bos.unlink(self._v2a("stat", vpath, *a)[1]) - vfs, ap = self._v2a( - "delete", vpath, [True, False, False, True] - ) + vfs, ap = self._v2a("delete", vpath, [True, False, False, True]) try: inf = bos.stat(ap) @@ -238,6 +303,7 @@ class Tftpd(object): fos.path.islink = self._hook fos.path.realpath = self._hook + def yeet(msg: str) -> None: warning(msg) raise TftpException(msg) diff --git a/copyparty/util.py b/copyparty/util.py index e4295f34..e3b7c6b1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -431,13 +431,24 @@ except: PY_DESC = py_desc() -VERSIONS = "copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {}".format( - S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER +VERSIONS = ( + "copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {}".format( + S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER + ) ) _: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER) -__all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER", "PARTFTPY_VER"] +__all__ = [ + "mp", + "BytesIO", + "quote", + "unquote", + "SQLITE_VER", + "JINJA_VER", + "PYFTPD_VER", + "PARTFTPY_VER", +] class Daemon(threading.Thread): diff --git a/tests/test_dots.py b/tests/test_dots.py index 5822dfdd..3e8e60a0 100644 --- a/tests/test_dots.py +++ b/tests/test_dots.py @@ -11,8 +11,8 @@ import unittest from copyparty.authsrv import AuthSrv from copyparty.httpcli import HttpCli -from copyparty.up2k import Up2k from copyparty.u2idx import U2idx +from copyparty.up2k import Up2k from tests import util as tu from tests.util import Cfg diff --git a/tests/util.py b/tests/util.py index 60b955da..a91c1cce 100644 --- a/tests/util.py +++ b/tests/util.py @@ -43,8 +43,8 @@ if MACOS: from copyparty.__init__ import E from copyparty.__main__ import init_E -from copyparty.util import FHC, Garda, Unrecv from copyparty.u2idx import U2idx +from copyparty.util import FHC, Garda, Unrecv init_E(E) From 02879713a29ef26de9799d28639531c6b9614fbe Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 12 Feb 2024 05:39:54 +0100 Subject: [PATCH 04/36] tftp: update readme + small py2 fix --- README.md | 13 +++++++++---- copyparty/tftpd.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 37c1bb94..9429a6cc 100644 --- a/README.md +++ b/README.md @@ -947,19 +947,24 @@ known client bugs: ## tftp server -a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 80s (in which case we should definitely hang some time)) +a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 90s (in which case we should definitely hang some time)) + +> that makes this the first RTX DECT Base that has been updated using copyparty ๐ŸŽ‰ * based on [partftpy](https://github.com/9001/partftpy) +* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable * needs a dedicated port (cannot share with the HTTP/HTTPS API) * run as root to use the spec-recommended port `69` (nice) -* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable +* can reply from a predefined portrange (good for firewalls) +* only supports the binary/octet/image transfer mode (no netascii) * [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported (will be extremely slow over WAN) + * expect ~1100 KiB/s over 1000BASE-T, 400~500 KiB/s over wifi, ~200 on bad wifi some recommended TFTP clients: * windows: `tftp.exe` (you probably already have it) * linux: `tftp-hpa`, `atftp` - * `tftp 127.0.0.1 3969 -v -m binary -c put initrd.bin` -* `curl` (read-only) + * `tftp 127.0.0.1 3969 -v -m binary -c put firmware.bin` +* `curl tftp://127.0.0.1:3969/firmware.bin` (read-only) ## smb server diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 8022b88a..62d29144 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -57,7 +57,7 @@ class Tftpd(object): self.asrv = hub.asrv self.log = hub.log - _hub.clear() + _hub[:] = [] _hub.append(hub) lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) From f7a43a8e4605634d307585ec5c4a860d8f04b6cd Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 12 Feb 2024 05:40:18 +0100 Subject: [PATCH 05/36] fix grid layout on first toggle from listview --- copyparty/web/browser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 562a05a3..655b7c6f 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4812,6 +4812,7 @@ var thegrid = (function () { r.dirty = false; r.bagit('#ggrid'); r.loadsel(); + aligngriditems(); setTimeout(r.tippen, 20); } From 7c8e368721f06bf961f0f69b86fd948a81191f52 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 12 Feb 2024 06:01:09 +0100 Subject: [PATCH 06/36] lol markdown --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9429a6cc..14c089aa 100644 --- a/README.md +++ b/README.md @@ -957,8 +957,8 @@ a TFTP server (read/write) can be started using `--tftp 3969` (you probably wan * run as root to use the spec-recommended port `69` (nice) * can reply from a predefined portrange (good for firewalls) * only supports the binary/octet/image transfer mode (no netascii) -* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported (will be extremely slow over WAN) - * expect ~1100 KiB/s over 1000BASE-T, 400~500 KiB/s over wifi, ~200 on bad wifi +* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN + * expect 1100 KiB/s over 1000BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi some recommended TFTP clients: * windows: `tftp.exe` (you probably already have it) From 6f8a588c4d473069baa4e42c1caef028a6bf4e98 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 13 Feb 2024 19:24:06 +0000 Subject: [PATCH 07/36] up2k: fix a mostly-harmless race as each chunk is written to the file, httpcli calls up2k.confirm_chunk to register the chunk as completed, and the reply indicates whether that was the final outstanding chunk, in which case httpcli closes the file descriptors since there's nothing more to write the issue is that the final chunk is registered as completed before the file descriptors are closed, meaning there could be writes that haven't finished flushing to disk yet if the client decides to issue another handshake during this window, up2k sees that all chunks are complete and calls up2k.finish_upload even as some threads might still be flushing the final writes to disk so the conditions to hit this bug were as follows (all must be true): * multiprocessing is disabled * there is a reverse-proxy * a client has several idle connections and reuses one of those * the server's filesystem is EXTREMELY slow, to the point where closing a file takes over 30 seconds the fix is to stop handshakes from being processed while a file is being closed, which is unfortunately a small bottleneck in that it prohibits initiating another upload while one is being finalized, but the required complexity to handle this better is probably not worth it (a separate mutex for each upload session or something like that) this issue is mostly harmless, partially because it is super tricky to hit (only aware of it happening synthetically), and because there is usually no harmful consequences; the worst-case is if this were to happen exactly as the server OS decides to crash, which would make the file appear to be fully uploaded even though it's missing some data (all extremely unlikely, but not impossible) there is no performance impact; if anything it should now accept new tcp connections slightly faster thanks to more granular locking --- copyparty/httpcli.py | 15 +++++++++------ copyparty/httpconn.py | 2 +- copyparty/httpsrv.py | 3 ++- tests/util.py | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index ef7b7908..153a6763 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -115,7 +115,7 @@ class HttpCli(object): self.t0 = time.time() self.conn = conn - self.mutex = conn.mutex # mypy404 + self.u2mutex = conn.u2mutex # mypy404 self.s = conn.s self.sr = conn.sr self.ip = conn.addr[0] @@ -1988,8 +1988,11 @@ class HttpCli(object): except: raise Pebkac(500, min_ex()) - x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps) - ret = x.get() + # not to protect u2fh, but to prevent handshakes while files are closing + with self.u2mutex: + x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps) + ret = x.get() + if self.is_vproxied: if "purl" in ret: ret["purl"] = self.args.SR + ret["purl"] @@ -2094,7 +2097,7 @@ class HttpCli(object): f = None fpool = not self.args.no_fpool and sprs if fpool: - with self.mutex: + with self.u2mutex: try: f = self.u2fh.pop(path) except: @@ -2137,7 +2140,7 @@ class HttpCli(object): if not fpool: f.close() else: - with self.mutex: + with self.u2mutex: self.u2fh.put(path, f) except: # maybe busted handle (eg. disk went full) @@ -2156,7 +2159,7 @@ class HttpCli(object): return False if not num_left and fpool: - with self.mutex: + with self.u2mutex: self.u2fh.close(path) if not num_left and not self.args.nw: diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 90f40a93..bf3690ab 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -50,7 +50,7 @@ class HttpConn(object): self.addr = addr self.hsrv = hsrv - self.mutex: threading.Lock = hsrv.mutex # mypy404 + self.u2mutex: threading.Lock = hsrv.u2mutex # mypy404 self.args: argparse.Namespace = hsrv.args # mypy404 self.E: EnvParams = self.args.E self.asrv: AuthSrv = hsrv.asrv # mypy404 diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 6e7daf9e..40bd108f 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -117,6 +117,7 @@ class HttpSrv(object): self.bound: set[tuple[str, int]] = set() self.name = "hsrv" + nsuf self.mutex = threading.Lock() + self.u2mutex = threading.Lock() self.stopping = False self.tp_nthr = 0 # actual @@ -220,7 +221,7 @@ class HttpSrv(object): def periodic(self) -> None: while True: time.sleep(2 if self.tp_ncli or self.ncli else 10) - with self.mutex: + with self.u2mutex, self.mutex: self.u2fh.clean() if self.tp_q: self.tp_ncli = max(self.ncli, self.tp_ncli - 2) diff --git a/tests/util.py b/tests/util.py index a91c1cce..3f0967ee 100644 --- a/tests/util.py +++ b/tests/util.py @@ -243,6 +243,7 @@ class VHttpConn(object): self.log_func = log self.log_src = "a" self.mutex = threading.Lock() + self.u2mutex = threading.Lock() self.nbyte = 0 self.nid = None self.nreq = -1 From 5d92f4df490c54c7086f67e4237b37c44a1b77e5 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 13 Feb 2024 19:47:42 +0000 Subject: [PATCH 08/36] mention why `-j0` can be a bad idea to enable, and that `--hist` can also help for loading thumbnails faster --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14c089aa..e0793acc 100644 --- a/README.md +++ b/README.md @@ -1697,6 +1697,7 @@ below are some tweaks roughly ordered by usefulness: * `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file * `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set + * and also makes thumbnails load faster, regardless of e2d/e2t * `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable * `--no-htp --hash-mt=0 --mtag-mt=1 --th-mt=1` minimizes the number of threads; can help in some eccentric environments (like the vscode debugger) * `-j0` enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example: @@ -1704,7 +1705,7 @@ below are some tweaks roughly ordered by usefulness: * simultaneous downloads and uploads saturating a 20gbps connection * if `-e2d` is enabled, `-j2` gives 4x performance for directory listings; `-j4` gives 16x - ...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u + ...however it also increases the server/filesystem/HDD load during uploads, and adds an overhead to internal communication, so it is usually a better idea to don't * using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others * and pypy can sometimes crash on startup with `-j0` (TODO make issue) From d4da386172947289cf30e18e9e6d5dc3dde4102b Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 14 Feb 2024 20:18:36 +0000 Subject: [PATCH 09/36] add watchdog for sqlite deadlock on db init: some cifs servers cause sqlite to fail in interesting ways; any attempt to create a table can instantly throw an exception, which results in a zerobyte database being created. During the next startup, the db would be determined to be corrupted, and up2k would invoke _backup_db before deleting and recreating it -- except that sqlite's connection.backup() will hang indefinitely and deadlock up2k add a watchdog which fires if it takes longer than 1 minute to open the database, printing a big warning that the filesystem probably does not support locking or is otherwise sqlite-incompatible, then writing a stacktrace of all threads to a textfile in the config directory (in case this deadlock is due to something completely different), before finally crashing spectacularly additionally, delete the database if the creation fails, which should prevents the deadlock on the next startup, so combine that with a message hinting at the filesystem incompatibility the 1-minute limit may sound excessively gracious, but considering what some of the copyparty instances out there is running on, really isn't this was reported when connecting to a cifs server running alpine thx to abex on discord for the detailed bug report! --- copyparty/up2k.py | 78 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index cb2ad273..f71e5a6e 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -21,7 +21,7 @@ from copy import deepcopy from queue import Queue -from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS +from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E from .authsrv import LEELOO_DALLAS, SSEELOG, VFS, AuthSrv from .bos import bos from .cfg import vf_bmap, vf_cmap, vf_vmap @@ -35,6 +35,7 @@ from .util import ( Pebkac, ProgressPrinter, absreal, + alltrace, atomic_move, db_ex_chk, dir_is_empty, @@ -87,6 +88,9 @@ zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png CV_EXTS = set(zsg.split(",")) +HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)" + + class Dbw(object): def __init__(self, c: "sqlite3.Cursor", n: int, t: float) -> None: self.c = c @@ -892,7 +896,7 @@ class Up2k(object): return None try: - cur = self._open_db(db_path) + cur = self._open_db_wd(db_path) # speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb) dbd = flags["dbd"] @@ -935,8 +939,8 @@ class Up2k(object): return cur, db_path except: - msg = "cannot use database at [{}]:\n{}" - self.log(msg.format(ptop, traceback.format_exc())) + msg = "ERROR: cannot use database at [%s]:\n%s\n\033[33mhint: %s\n" + self.log(msg % (db_path, traceback.format_exc(), HINT_HISTPATH), 1) return None @@ -2155,6 +2159,46 @@ class Up2k(object): def _trace(self, msg: str) -> None: self.log("ST: {}".format(msg)) + def _open_db_wd(self, db_path: str) -> "sqlite3.Cursor": + ok: list[int] = [] + Daemon(self._open_db_timeout, "opendb_watchdog", [db_path, ok]) + try: + return self._open_db(db_path) + finally: + ok.append(1) + + def _open_db_timeout(self, db_path, ok: list[int]) -> None: + # give it plenty of time due to the count statement (and wisdom from byte's box) + for _ in range(60): + time.sleep(1) + if ok: + return + + t = "WARNING:\n\n initializing an up2k database is taking longer than one minute; something has probably gone wrong:\n\n" + self._log_sqlite_incompat(db_path, t) + + def _log_sqlite_incompat(self, db_path, t0) -> None: + txt = t0 or "" + digest = hashlib.sha512(db_path.encode("utf-8", "replace")).digest() + stackname = base64.urlsafe_b64encode(digest[:9]).decode("utf-8") + stackpath = os.path.join(E.cfg, "stack-%s.txt" % (stackname,)) + + t = " the filesystem at %s may not support locking, or is otherwise incompatible with sqlite\n\n %s\n\n" + t += " PS: if you think this is a bug and wish to report it, please include your configuration + the following file: %s\n" + txt += t % (db_path, HINT_HISTPATH, stackpath) + self.log(txt, 3) + + try: + stk = alltrace() + with open(stackpath, "wb") as f: + f.write(stk.encode("utf-8", "replace")) + except Exception as ex: + self.log("warning: failed to write %s: %s" % (stackpath, ex), 3) + + if self.args.q: + t = "-" * 72 + raise Exception("%s\n%s\n%s" % (t, txt, t)) + def _orz(self, db_path: str) -> "sqlite3.Cursor": c = sqlite3.connect( db_path, timeout=self.timeout, check_same_thread=False @@ -2167,7 +2211,7 @@ class Up2k(object): cur = self._orz(db_path) ver = self._read_ver(cur) if not existed and ver is None: - return self._create_db(db_path, cur) + return self._try_create_db(db_path, cur) if ver == 4: try: @@ -2205,8 +2249,16 @@ class Up2k(object): db = cur.connection cur.close() db.close() - bos.unlink(db_path) - return self._create_db(db_path, None) + self._delete_db(db_path) + return self._try_create_db(db_path, None) + + def _delete_db(self, db_path: str): + for suf in ("", "-shm", "-wal", "-journal"): + try: + bos.unlink(db_path + suf) + except: + if not suf: + raise def _backup_db( self, db_path: str, cur: "sqlite3.Cursor", ver: Optional[int], msg: str @@ -2243,6 +2295,18 @@ class Up2k(object): return int(rows[0][0]) return None + def _try_create_db( + self, db_path: str, cur: Optional["sqlite3.Cursor"] + ) -> "sqlite3.Cursor": + try: + return self._create_db(db_path, cur) + except: + try: + self._delete_db(db_path) + except: + pass + raise + def _create_db( self, db_path: str, cur: Optional["sqlite3.Cursor"] ) -> "sqlite3.Cursor": From f262aee8008dd9b9ae37c563991ff0d9e2057b08 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 14 Feb 2024 22:44:33 +0000 Subject: [PATCH 10/36] change folders to preload music when necessary: on phones especially, hitting the end of a folder while playing music could permanently stop audio playback, because the browser will revoke playback privileges unless we have a song ready to go... there's no time to navigate through folders looking for the next file the preloader will now start jumping through folders ahead of time --- copyparty/web/browser.css | 4 +++- copyparty/web/browser.js | 50 +++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 656fdb50..737e0a0f 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1775,6 +1775,7 @@ html.y #tree.nowrap .ntree a+a:hover { padding: 0; } #thumbs, +#au_prescan, #au_fullpre, #au_os_seek, #au_osd_cv, @@ -1782,7 +1783,8 @@ html.y #tree.nowrap .ntree a+a:hover { opacity: .3; } #griden.on+#thumbs, -#au_preload.on+#au_fullpre, +#au_preload.on+#au_prescan, +#au_preload.on+#au_prescan+#au_fullpre, #au_os_ctl.on+#au_os_seek, #au_os_ctl.on+#au_os_seek+#au_osd_cv, #u2turbo.on+#u2tdate { diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 655b7c6f..932337bb 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -240,6 +240,7 @@ var Ls = { "mt_shuf": "shuffle the songs in each folder\">๐Ÿ”€", "mt_preload": "start loading the next song near the end for gapless playback\">preload", + "mt_prescan": "go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\">nav", "mt_fullpre": "try to preload the entire song;$Nโœ… enable on unreliable connections,$NโŒ disable on slow connections probably\">full", "mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s", "mt_npclip": "show buttons for clipboarding the currently playing song\">/np", @@ -272,6 +273,8 @@ var Ls = { "mm_e403": "Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out", "mm_e5xx": "Could not play audio; server error ", "mm_nof": "not finding any more audio files nearby", + "mm_prescan": "Looking for music to play next...", + "mm_scank": "Found the next song:", "mm_uncache": "cache cleared; all songs will redownload on next playback", "mm_pwrsv": "

it looks like playback is being interrupted by your phone's power-saving settings!

" + '

please go to the app settings of your browser and then allow unrestricted battery usage to fix it.

however, it could also be due to the browser\'s autoplay settings;

Firefox: tap the icon on the left side of the address bar, then select "autoplay" and "allow audio"

Chrome: the problem will gradually dissipate as you play more music on this site

', "mm_iosblk": "

your web browser thinks the audio playback is unwanted, and it decided to block playback until you start another track manually... unfortunately we are both powerless in telling it otherwise

supposedly this will get better as you continue playing music on this site, but I'm unfamiliar with apple devices so idk if that's true

you could try another browser, maybe firefox or chrome?

", @@ -732,6 +735,7 @@ var Ls = { "mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefรธlge\">๐Ÿ”€", "mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles", + "mt_prescan": "ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette รฅ spille musikk\">bla", "mt_fullpre": "hent ned hele neste sang, ikke bare litt:$Nโœ… skru pรฅ hvis nettet ditt er ustabilt,$NโŒ skru av hvis nettet ditt er tregt\">full", "mt_waves": "waveform seekbar:$Nvis volumkurve i avspillingsfeltet\">~s", "mt_npclip": "vis knapper for รฅ kopiere info om sangen du hรธrer pรฅ\">/np", @@ -764,6 +768,8 @@ var Ls = { "mm_e403": "Avspilling feilet: Tilgang nektet.\n\nKanskje du ble logget ut?\nPrรธv รฅ trykk F5 for รฅ laste siden pรฅ nytt.", "mm_e5xx": "Avspilling feilet: ", "mm_nof": "finner ikke flere sanger i nรฆrheten", + "mm_prescan": "Leter etter neste sang...", + "mm_scank": "Fant neste sang:", "mm_uncache": "alle sanger vil lastes pรฅ nytt ved neste avspilling", "mm_pwrsv": "

det ser ut som musikken ble avbrutt av telefonen sine strรธmsparings-innstillinger!

" + '

ta en tur innom app-innstillingene til nettleseren din og sรฅ tillat ubegrenset batteriforbruk

NB: det kan ogsรฅ vรฆre pga. autoplay-innstillingene, sรฅ prรธv dette:

Firefox: klikk pรฅ ikonet i venstre side av addressefeltet, velg "autoplay" og "tillat lyd"

Chrome: problemet vil minske gradvis jo mer musikk du spiller pรฅ denne siden

', "mm_iosblk": "

nettleseren din tror at musikken er uรธnsket, og den bestemte seg for รฅ stoppe avspillingen slik at du manuelt mรฅ velge en ny sang... dessverre er bรฅde du og jeg makteslรธse nรฅr den har bestemt seg.

det ryktes at problemet vil minske jo mer musikk du spiller pรฅ denne siden, men jeg er ikke godt kjent med apple-dingser sรฅ jeg er ikke sikker.

kanskje firefox eller chrome fungerer bedre?

", @@ -1391,6 +1397,7 @@ var mpl = (function () { '

' + L.cl_opts + '

' + '= mp.order.length) { + if (!mpl.prescan) + throw "prescan disabled"; + + if (mpl.prescan_evp == evp) + throw "evp match"; + + mpl.prescan_evp = evp; + toast.inf(10, L.mm_prescan); + treectl.ls_cb = repreload; + tree_neigh(1); + } + else + mp.preload(mp.tracks[mp.order[oi]], full); } catch (ex) { console.log("preload failed", ex); @@ -3039,6 +3080,7 @@ function play(tid, is_ev, seek) { }, 500); mp.au.tid = tid; + mp.au.evp = get_evpath(); mp.au.volume = mp.expvol(mp.vol); var trs = QSA('#files tr.play'); for (var a = 0, aa = trs.length; a < aa; a++) From 64ad5853188d0468682bd100155f503cb7dec024 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 14 Feb 2024 23:08:32 +0000 Subject: [PATCH 11/36] ie11: file selection hotkeys --- copyparty/web/browser.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 932337bb..2d49f450 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -5089,23 +5089,24 @@ document.onkeydown = function (e) { return ebi('griden').click(); } - if (aet == 'tr' && ae.closest('#files')) { + if ((aet == 'tr' || aet == 'td') && ae.closest('#files')) { var d = '', rem = 0; - if (k == 'ArrowUp') d = 'previous'; - if (k == 'ArrowDown') d = 'next'; + if (aet == 'td') ae = ae.closest('tr'); //ie11 + if (k == 'ArrowUp' || k == 'Up') d = 'previous'; + if (k == 'ArrowDown' || k == 'Down') d = 'next'; if (k == 'PageUp') { d = 'previous'; rem = 0.6; } if (k == 'PageDown') { d = 'next'; rem = 0.6; } if (d) { fselfunw(e, ae, d, rem); return ev(e); } - if (k == 'Space') { + if (k == 'Space' || k == 'Spacebar') { clmod(ae, 'sel', 't'); msel.origin_tr(ae); msel.selui(); return ev(e); } - if (k == 'KeyA' && ctrl(e)) { + if ((k == 'KeyA' || k == 'a') && ctrl(e)) { var sel = msel.getsel(), all = msel.getall(); @@ -5116,7 +5117,7 @@ document.onkeydown = function (e) { } if (ae && ae.closest('pre')) { - if (k == 'KeyA' && ctrl(e)) { + if ((k == 'KeyA' || k == 'a') && ctrl(e)) { var sel = document.getSelection(), ran = document.createRange(); From 879e83e24f30cc660f154bfe5c200cdf58ed235e Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 14 Feb 2024 23:26:06 +0000 Subject: [PATCH 12/36] ignore easymde errors it randomly throws when clicking inside the preview pane --- copyparty/web/util.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/copyparty/web/util.js b/copyparty/web/util.js index 057b8ad9..1faf73be 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -157,6 +157,10 @@ catch (ex) { } var crashed = false, ignexd = {}, evalex_fatal = false; function vis_exh(msg, url, lineNo, columnNo, error) { + var ekey = url + '\n' + lineNo + '\n' + msg; + if (ignexd[ekey] || crashed) + return; + msg = String(msg); url = String(url); @@ -175,9 +179,8 @@ function vis_exh(msg, url, lineNo, columnNo, error) { if (IE && url.indexOf('prism.js') + 1) return; - var ekey = url + '\n' + lineNo + '\n' + msg; - if (ignexd[ekey] || crashed) - return; + if (url.indexOf('easymde.js') + 1) + return; // clicking the preview pane if (url.indexOf('deps/marked.js') + 1 && !window.WebAssembly) return; // ff<52 From a0da0122b993267a971a2575195ee2f59709ed72 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 15 Feb 2024 00:00:41 +0000 Subject: [PATCH 13/36] v1.10.0 --- .vscode/launch.json | 3 +-- README.md | 2 +- copyparty/__version__.py | 6 +++--- docs/changelog.md | 22 ++++++++++++++++++++++ pyproject.toml | 1 + scripts/make-pypi-release.sh | 15 ++++++++------- setup.py | 1 + 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c611c5a1..9ab69494 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,8 +19,7 @@ "-emp", "-e2dsa", "-e2ts", - "-mtp", - ".bpm=f,bin/mtag/audio-bpm.py", + "-mtp=.bpm=f,bin/mtag/audio-bpm.py", "-aed:wark", "-vsrv::r:rw,ed:c,dupe", "-vdist:dist:r" diff --git a/README.md b/README.md index e0793acc..7ef075bd 100644 --- a/README.md +++ b/README.md @@ -1714,7 +1714,7 @@ below are some tweaks roughly ordered by usefulness: when uploading files, -* chrome is recommended, at least compared to firefox: +* chrome is recommended (unfortunately), at least compared to firefox: * up to 90% faster when hashing, especially on SSDs * up to 40% faster when uploading over extremely fast internets * but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 0a1c5ec3..8a29f469 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 9, 31) -CODENAME = "prometheable" -BUILD_DT = (2024, 2, 3) +VERSION = (1, 10, 0) +CODENAME = "tftp" +BUILD_DT = (2024, 2, 15) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index 8ce72ec8..ba035215 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,25 @@ +โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ +# 2024-0203-1533 `v1.9.31` eject + +## new features + +* disable mkdir / new-doc buttons until a name is provided d3db6d29 +* warning about browsers limiting the number of connections c354a38b + +## bugfixes + +* #71 stop videos from buffering in the background a17c267d +* improve up2k ETA on slow networks / many connections c1180d6f +* u2c: exclude-filter didn't apply to file deletions b2e23340 +* `--touch` / `re๐Ÿ“…` didn't apply to zerobyte files 945170e2 + +## other changes + +* notes on [hardlink/symlink conversion](https://github.com/9001/copyparty/blob/6c2c6090/docs/notes.sh#L35-L46) 6c2c6090 +* [lore](https://github.com/9001/copyparty/blob/hovudstraum/docs/notes.md#trivia--lore) b1cf5884 + + + โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ # 2024-0125-2252 `v1.9.30` retime diff --git a/pyproject.toml b/pyproject.toml index 9e58d831..77e25380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: OS Independent", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Intended Audience :: End Users/Desktop", diff --git a/scripts/make-pypi-release.sh b/scripts/make-pypi-release.sh index b3df98b5..28c0764c 100755 --- a/scripts/make-pypi-release.sh +++ b/scripts/make-pypi-release.sh @@ -77,13 +77,14 @@ function have() { } function load_env() { - . buildenv/bin/activate - have setuptools - have wheel - have build - have twine - have jinja2 - have strip_hints + . buildenv/bin/activate || return 1 + have setuptools && + have wheel && + have build && + have twine && + have jinja2 && + have strip_hints && + return 0 || return 1 } load_env || { diff --git a/setup.py b/setup.py index 4cc3cd2a..4e52add3 100755 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ args = { "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: OS Independent", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Intended Audience :: End Users/Desktop", From 39cc92d4bc033c39703ca49d317c286bf2296518 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 15 Feb 2024 00:56:37 +0000 Subject: [PATCH 14/36] update pkgs to 1.10.0 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 34c86870..c28957b1 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: icxes pkgname=copyparty -pkgver="1.9.31" +pkgver="1.10.0" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, 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=("a8ec1faf8cb224515355226882fdb2d1ab1de42d96ff78e148b930318867a71e") +sha256sums=("a44338b24d28fd6962504ec2c0e466e16144ddd4c5af44eb3ae493534152fd07") build() { cd "${srcdir}/${pkgname}-${pkgver}" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 6845ad7e..fc4f34a1 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.9.31/copyparty-sfx.py", - "version": "1.9.31", - "hash": "sha256-yp7qoiW5yzm2M7qVmYY7R+SyhZXlqL+JxsXV22aS+MM=" + "url": "https://github.com/9001/copyparty/releases/download/v1.10.0/copyparty-sfx.py", + "version": "1.10.0", + "hash": "sha256-pPkiDKXEv7P1zPB7/BSo85St0FNnhUpb30sxIi7mn1c=" } \ No newline at end of file From 0504b010a13b14e515a698c1ed94af3a00dcbf41 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Feb 2024 21:31:58 +0000 Subject: [PATCH 15/36] tftp: support ipv6 and utf-8 filenames + ... * fix winexe * missing newline after dirlist * optimizations --- README.md | 17 ++++-- copyparty/__main__.py | 1 + copyparty/tftpd.py | 100 +++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- scripts/make-sfx.sh | 4 +- scripts/pyinstaller/build.sh | 2 +- scripts/test/tftp.sh | 36 +++++++++++++ setup.py | 2 +- 8 files changed, 145 insertions(+), 19 deletions(-) create mode 100755 scripts/test/tftp.sh diff --git a/README.md b/README.md index 7ef075bd..6f7951a1 100644 --- a/README.md +++ b/README.md @@ -954,17 +954,24 @@ a TFTP server (read/write) can be started using `--tftp 3969` (you probably wan * based on [partftpy](https://github.com/9001/partftpy) * no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable * needs a dedicated port (cannot share with the HTTP/HTTPS API) - * run as root to use the spec-recommended port `69` (nice) + * run as root (or see below) to use the spec-recommended port `69` (nice) * can reply from a predefined portrange (good for firewalls) * only supports the binary/octet/image transfer mode (no netascii) * [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN - * expect 1100 KiB/s over 1000BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi + * assuming default blksize (512), expect 1100 KiB/s over 100BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi + +most clients expect to find TFTP on port 69, but on linux and macos you need to be root to listen on that. Alternatively, listen on 3969 and use NAT on the server to forward 69 to that port; +* on linux: `iptables -t nat -A PREROUTING -i eth0 -p udp --dport 69 -j REDIRECT --to-port 3969` some recommended TFTP clients: +* curl (cross-platform, read/write) + * get: `curl --tftp-blksize 1428 tftp://127.0.0.1:3969/firmware.bin` + * put: `curl --tftp-blksize 1428 -T firmware.bin tftp://127.0.0.1:3969/` * windows: `tftp.exe` (you probably already have it) + * `tftp -i 127.0.0.1 put firmware.bin` * linux: `tftp-hpa`, `atftp` - * `tftp 127.0.0.1 3969 -v -m binary -c put firmware.bin` -* `curl tftp://127.0.0.1:3969/firmware.bin` (read-only) + * `atftp --option "blksize 1428" 127.0.0.1 3969 -p -l firmware.bin -r firmware.bin` + * `tftp -v -m binary 127.0.0.1 3969 -c put firmware.bin` ## smb server @@ -997,7 +1004,7 @@ known client bugs: * however smb1 is buggy and is not enabled by default on win10 onwards * windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.` -the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT to forward the traffic from 445 to there; +the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT on the server to forward the traffic from 445 to there; * on linux: `iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 445 -j REDIRECT --to-port 3945` authenticate with one of the following: diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 561f33c9..f4faa5ce 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1019,6 +1019,7 @@ def add_tftp(ap): ap2.add_argument("--tftp", metavar="PORT", type=int, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpvv", action="store_true", help="verboser") + ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations") ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") ap2.add_argument("--tftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]") diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 62d29144..ac1aaedf 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -10,19 +10,33 @@ except: self.__dict__.update(attr) -import inspect import logging import os +import re +import socket import stat +import threading +import time from datetime import datetime -from partftpy import TftpContexts, TftpServer, TftpStates +try: + import inspect +except: + pass + +from partftpy import ( + TftpContexts, + TftpPacketFactory, + TftpPacketTypes, + TftpServer, + TftpStates, +) from partftpy.TftpShared import TftpException -from .__init__ import PY2, TYPE_CHECKING +from .__init__ import EXE, TYPE_CHECKING from .authsrv import VFS from .bos import bos -from .util import BytesIO, Daemon, exclude_dotfiles, runhook, undot +from .util import BytesIO, Daemon, exclude_dotfiles, min_ex, runhook, undot if True: # pylint: disable=using-constant-test from typing import Any, Union @@ -35,6 +49,10 @@ lg = logging.getLogger("tftp") debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error) +def noop(*a, **ka) -> None: + pass + + def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool: info("connection from %s:%s", raddress, rport) ret = _orig_serverInitial(self, pkt, raddress, rport) @@ -56,6 +74,7 @@ class Tftpd(object): self.args = hub.args self.asrv = hub.asrv self.log = hub.log + self.mutex = threading.Lock() _hub[:] = [] _hub.append(hub) @@ -65,6 +84,38 @@ class Tftpd(object): lgr = logging.getLogger(x) lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) + if not self.args.tftpv and not self.args.tftpvv: + # contexts -> states -> packettypes -> shared + # contexts -> packetfactory + # packetfactory -> packettypes + Cs = [ + TftpPacketTypes, + TftpPacketFactory, + TftpStates, + TftpContexts, + TftpServer, + ] + cbak = [] + if not self.args.tftp_no_fast and not EXE: + try: + import inspect + + ptn = re.compile(r"(^\s*)log\.debug\(.*\)$") + for C in Cs: + cbak.append(C.__dict__) + src1 = inspect.getsource(C).split("\n") + src2 = "\n".join([ptn.sub("\\1pass", ln) for ln in src1]) + cfn = C.__spec__.origin + exec (compile(src2, filename=cfn, mode="exec"), C.__dict__) + except Exception: + t = "failed to optimize tftp code; run with --tftp-noopt if there are issues:\n" + self.log("tftp", t + min_ex(), 3) + for n, zd in enumerate(cbak): + Cs[n].__dict__ = zd + + for C in Cs: + C.log.debug = noop + # patch vfs into partftpy TftpContexts.open = self._open TftpStates.open = self._open @@ -102,21 +153,52 @@ class Tftpd(object): self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3) ip = "0.0.0.0" - self.ip = ip self.port = int(self.args.tftp) - self.srv = TftpServer.TftpServer("/", self._ls) - self.stop = self.srv.stop + self.srv = [] + self.ips = [] ports = [] if self.args.tftp_pr: p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")] ports = list(range(p1, p2 + 1)) - Daemon(self.srv.listen, "tftp", [self.ip, self.port], ka={"ports": ports}) + ips = self.args.i + if "::" in ips: + ips.append("0.0.0.0") + + if self.args.ftp4: + ips = [x for x in ips if ":" not in x] + + for ip in ips: + name = "tftp_%s" % (ip,) + Daemon(self._start, name, [ip, ports]) + time.sleep(0.2) # give dualstack a chance def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("tftp", msg, c) + def _start(self, ip, ports): + fam = socket.AF_INET6 if ":" in ip else socket.AF_INET + srv = TftpServer.TftpServer("/", self._ls) + with self.mutex: + self.srv.append(srv) + self.ips.append(ip) + try: + srv.listen(ip, self.port, af_family=fam, ports=ports) + except OSError: + with self.mutex: + self.srv.remove(srv) + self.ips.remove(ip) + if ip != "0.0.0.0" or "::" not in self.ips: + raise + + def stop(self): + with self.mutex: + srvs = self.srv[:] + + for srv in srvs: + srv.stop() + def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]: vpath = vpath.replace("\\", "/").lstrip("/") if not perms: @@ -190,7 +272,7 @@ class Tftpd(object): retl = ["# permissions: %s" % (", ".join(perms),)] retl += [fmt.format(*x) for x in ls] ret = "\n".join(retl).encode("utf-8", "replace") - return BytesIO(ret) + return BytesIO(ret + b"\n") def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any: rd = wr = False diff --git a/pyproject.toml b/pyproject.toml index 77e25380..5b23c62a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ thumbnails2 = ["pyvips"] audiotags = ["mutagen"] ftpd = ["pyftpdlib"] ftps = ["pyftpdlib", "pyopenssl"] -tftpd = ["partftpy>=0.2.0"] +tftpd = ["partftpy>=0.3.0"] pwhash = ["argon2-cffi"] [project.scripts] diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index 44f26938..0c77f5f2 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -225,9 +225,9 @@ necho() { mv pyftpdlib ftp/ necho collecting partftpy - f="../build/partftpy-0.2.0.tar.gz" + f="../build/partftpy-0.3.0.tar.gz" [ -e "$f" ] || - (url=https://files.pythonhosted.org/packages/64/4a/360dde1e7277758a4ccb0d6434ec661042d9d745aa6c3baa9ec0699df3e9/partftpy-0.2.0.tar.gz; + (url=https://files.pythonhosted.org/packages/06/ce/531978c831c47f79bc72d5bbb3f12757daf1602d1fffad012305f2d270f6/partftpy-0.3.0.tar.gz; wget -O$f "$url" || curl -L "$url" >$f) tar -zxf $f diff --git a/scripts/pyinstaller/build.sh b/scripts/pyinstaller/build.sh index 5bf0c5c3..adf94c98 100644 --- a/scripts/pyinstaller/build.sh +++ b/scripts/pyinstaller/build.sh @@ -37,7 +37,7 @@ rm -rf $TEMP/pe-copyparty* python copyparty-sfx.py --version rm -rf mods; mkdir mods -cp -pR $TEMP/pe-copyparty/copyparty/ $TEMP/pe-copyparty/{ftp,j2}/* mods/ +cp -pR $TEMP/pe-copyparty/{copyparty,partftpy}/ $TEMP/pe-copyparty/{ftp,j2}/* mods/ [ $w10 ] && rm -rf mods/{jinja2,markupsafe} af() { awk "$1" <$2 >tf; mv tf "$2"; } diff --git a/scripts/test/tftp.sh b/scripts/test/tftp.sh new file mode 100755 index 00000000..b52accb1 --- /dev/null +++ b/scripts/test/tftp.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -ex + +# PYTHONPATH=.:~/dev/partftpy/ taskset -c 0 python3 -m copyparty -v srv::r -v srv/junk:junk:A --tftp 3969 + +get_src=~/dev/copyparty/srv/palette.flac +get_fn=${get_src##*/} + +put_src=~/Downloads/102.zip +put_dst=~/dev/copyparty/srv/junk/102.zip + +cd /dev/shm + +echo curl get 1428 v4; curl --tftp-blksize 1428 tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1 +echo curl get 1428 v6; curl --tftp-blksize 1428 tftp://[::1]:3969/$get_fn | cmp $get_src || exit 1 + +echo curl put 1428 v4; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1 +echo curl put 1428 v6; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://[::1]:3969/junk/ && cmp $put_src $put_dst || exit 1 + +echo atftp get 1428; rm -f $get_fn && ~/src/atftp/atftp --option "blksize 1428" -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1 + +echo atftp put 1428; rm -f $put_dst && ~/src/atftp/atftp --option "blksize 1428" 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1 + +echo tftp-hpa get; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c get $get_fn && cmp $get_src $get_fn || exit 1 + +echo tftp-hpa put; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c put $put_src junk/102.zip && cmp $put_src $put_dst || exit 1 + +echo curl get 512; curl tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1 + +echo curl put 512; rm -f $put_dst && curl -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1 + +echo atftp get 512; rm -f $get_fn && ~/src/atftp/atftp -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1 + +echo atftp put 512; rm -f $put_dst && ~/src/atftp/atftp 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1 + +echo nice diff --git a/setup.py b/setup.py index 4e52add3..b6bf7576 100755 --- a/setup.py +++ b/setup.py @@ -141,7 +141,7 @@ args = { "audiotags": ["mutagen"], "ftpd": ["pyftpdlib"], "ftps": ["pyftpdlib", "pyopenssl"], - "tftpd": ["partftpy>=0.2.0"], + "tftpd": ["partftpy>=0.3.0"], "pwhash": ["argon2-cffi"], }, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, From 6bd087ddc5f3afb8de4863ad515c8a9dbfc17c8e Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Feb 2024 22:59:56 +0000 Subject: [PATCH 16/36] fix #72 (error deleting zerobyte files if db disabled) --- copyparty/up2k.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index f71e5a6e..c86f0417 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3680,9 +3680,10 @@ class Up2k(object): ) job = reg.get(wark) if wark else None if job: - t = "forgetting partial upload {} ({})" - p = self._vis_job_progress(job) - self.log(t.format(wark, p)) + if job["need"]: + t = "forgetting partial upload {} ({})" + p = self._vis_job_progress(job) + self.log(t.format(wark, p)) assert wark del reg[wark] From fd552842d441e12896d2ddc617fe22da3b9d541e Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Feb 2024 23:19:11 +0000 Subject: [PATCH 17/36] fix other possible division-by-zeros; u2c: also fix exe detection --- bin/u2c.py | 14 +++++++------- copyparty/authsrv.py | 2 +- copyparty/up2k.py | 2 +- copyparty/util.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index 4054532b..7076b17a 100755 --- a/bin/u2c.py +++ b/bin/u2c.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from __future__ import print_function, unicode_literals -S_VERSION = "1.14" -S_BUILD_DT = "2024-01-27" +S_VERSION = "1.15" +S_BUILD_DT = "2024-02-18" """ u2c.py: upload to copyparty @@ -29,7 +29,7 @@ import platform import threading import datetime -EXE = sys.executable.endswith("exe") +EXE = bool(getattr(sys, "frozen", False)) try: import argparse @@ -846,12 +846,12 @@ class Ctl(object): txt = " " if not self.up_br: - spd = self.hash_b / (time.time() - self.t0) - eta = (self.nbytes - self.hash_b) / (spd + 1) + spd = self.hash_b / ((time.time() - self.t0) or 1) + eta = (self.nbytes - self.hash_b) / (spd or 1) else: - spd = self.up_br / (time.time() - self.t0_up) + spd = self.up_br / ((time.time() - self.t0_up) or 1) spd = self.spd = (self.spd or spd) * 0.9 + spd * 0.1 - eta = (self.nbytes - self.up_b) / (spd + 1) + eta = (self.nbytes - self.up_b) / (spd or 1) spd = humansize(spd) self.eta = str(datetime.timedelta(seconds=int(eta))) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index cbb1d662..df945b63 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -193,7 +193,7 @@ class Lim(object): self.dft = int(time.time()) + 300 self.dfv = get_df(abspath)[0] or 0 for j in list(self.reg.values()) if self.reg else []: - self.dfv -= int(j["size"] / len(j["hash"]) * len(j["need"])) + self.dfv -= int(j["size"] / (len(j["hash"]) or 999) * len(j["need"])) if already_written: sz = 0 diff --git a/copyparty/up2k.py b/copyparty/up2k.py index c86f0417..430d6c93 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -552,7 +552,7 @@ class Up2k(object): runihook(self.log, cmd, vol, ups) def _vis_job_progress(self, job: dict[str, Any]) -> str: - perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"])) + perc = 100 - (len(job["need"]) * 100.0 / (len(job["hash"]) or 1)) path = djoin(job["ptop"], job["prel"], job["name"]) return "{:5.1f}% {}".format(perc, path) diff --git a/copyparty/util.py b/copyparty/util.py index e3b7c6b1..3be6c9c3 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1768,7 +1768,7 @@ def get_spd(nbyte: int, t0: float, t: Optional[float] = None) -> str: if t is None: t = time.time() - bps = nbyte / ((t - t0) + 0.001) + bps = nbyte / ((t - t0) or 0.001) s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "") s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "") return "%s \033[0m%s/s\033[0m" % (s1, s2) From 655f6d00f87828455be5b67ec4b9ec3448661908 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Feb 2024 23:24:31 +0000 Subject: [PATCH 18/36] faster tagscanning of zerobyte files --- copyparty/up2k.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 430d6c93..af834394 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -154,7 +154,7 @@ class Up2k(object): self.hashq: Queue[ tuple[str, str, dict[str, Any], str, str, str, float, str, bool] ] = Queue() - self.tagq: Queue[tuple[str, str, str, str, str, float]] = Queue() + self.tagq: Queue[tuple[str, str, str, str, int, str, float]] = Queue() self.tag_event = threading.Condition() self.hashq_mutex = threading.Lock() self.n_hashq = 0 @@ -2055,12 +2055,13 @@ class Up2k(object): return try: + st = bos.stat(qe.abspath) if not qe.mtp: if self.args.mtag_vv: t = "tag-thr: {}({})" self.log(t.format(self.mtag.backend, qe.abspath), "90") - tags = self.mtag.get(qe.abspath) + tags = self.mtag.get(qe.abspath) if st.st_size else {} else: if self.args.mtag_vv: t = "tag-thr: {}({})" @@ -2101,11 +2102,16 @@ class Up2k(object): """will mutex""" assert self.mtag - if not bos.path.isfile(abspath): + try: + st = bos.stat(abspath) + except: + return 0 + + if not stat.S_ISREG(st.st_mode): return 0 try: - tags = self.mtag.get(abspath) + tags = self.mtag.get(abspath) if st.st_size else {} except Exception as ex: self._log_tag_err("", abspath, ex) return 0 @@ -3098,7 +3104,7 @@ class Up2k(object): raise if "e2t" in self.flags[ptop]: - self.tagq.put((ptop, wark, rd, fn, ip, at)) + self.tagq.put((ptop, wark, rd, fn, sz, ip, at)) self.n_tagq += 1 return True @@ -4056,14 +4062,14 @@ class Up2k(object): with self.mutex: self.n_tagq -= 1 - ptop, wark, rd, fn, ip, at = self.tagq.get() + ptop, wark, rd, fn, sz, ip, at = self.tagq.get() if "e2t" not in self.flags[ptop]: continue # self.log("\n " + repr([ptop, rd, fn])) abspath = djoin(ptop, rd, fn) try: - tags = self.mtag.get(abspath) + tags = self.mtag.get(abspath) if sz else {} ntags1 = len(tags) parsers = self._get_parsers(ptop, tags, abspath) if self.args.mtag_vv: From 33f41f3e615d9676a07ab9895fa03273eb8497ee Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 13:04:22 +0000 Subject: [PATCH 19/36] add hi-res thumbs (togglebtn/servercfg) --- copyparty/__main__.py | 4 ++- copyparty/cfg.py | 6 ++-- copyparty/httpcli.py | 6 ++-- copyparty/ico.py | 2 +- copyparty/th_srv.py | 29 ++++++++++------- copyparty/web/browser.js | 70 ++++++++++++++++++++++++++++++---------- tests/util.py | 4 ++- 7 files changed, 85 insertions(+), 36 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f4faa5ce..60bb6919 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1170,7 +1170,8 @@ def add_thumbnail(ap): ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") - ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)") + ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32mfy\033[0m]=crop, [\033[32mfn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") + ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32mfy\033[0m]=yes, [\033[32mfn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") @@ -1430,6 +1431,7 @@ def main(argv: Optional[list[str]] = None) -> None: deprecated: list[tuple[str, str]] = [ ("--salt", "--warksalt"), ("--hdr-au-usr", "--idp-h-usr"), + ("--th-no-crop", "--th-crop=n"), ] for dk, nk in deprecated: idx = -1 diff --git a/copyparty/cfg.py b/copyparty/cfg.py index cc4f77a1..10e921e8 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -20,7 +20,6 @@ def vf_bmap() -> dict[str, str]: "no_thumb": "dthumb", "no_vthumb": "dvthumb", "no_athumb": "dathumb", - "th_no_crop": "nocrop", } for k in ( "dotsrch", @@ -56,6 +55,8 @@ def vf_vmap() -> dict[str, str]: "re_maxage": "scan", "th_convt": "convt", "th_size": "thsize", + "th_crop": "crop", + "th_x3": "th3x", } for k in ( "dbd", @@ -172,7 +173,8 @@ flagcats = { "dathumb": "disables audio thumbnails (spectrograms)", "dithumb": "disables image thumbnails", "thsize": "thumbnail res; WxH", - "nocrop": "disable center-cropping by default", + "crop": "center-cropping (y/n/fy/fn)", + "th3x": "3x resolution (y/n/fy/fn)", "convt": "conversion timeout in seconds", }, "handlers\n(better explained in --help-handlers)": { diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 153a6763..fc12be4f 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3973,7 +3973,8 @@ class HttpCli(object): "idx": e2d, "itag": e2t, "dsort": vf["sort"], - "dfull": "nocrop" in vf, + "dcrop": vf["crop"], + "dth3x": vf["th3x"], "u2ts": vf["u2ts"], "lifetime": vn.flags.get("lifetime") or 0, "frand": bool(vn.flags.get("rand")), @@ -4000,8 +4001,9 @@ class HttpCli(object): "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "readme": readme, "dgrid": "grid" in vf, - "dfull": "nocrop" in vf, "dsort": vf["sort"], + "dcrop": vf["crop"], + "dth3x": vf["th3x"], "themes": self.args.themes, "turbolvl": self.args.turbo, "u2j": self.args.u2j, diff --git a/copyparty/ico.py b/copyparty/ico.py index 00da00dd..28537d25 100644 --- a/copyparty/ico.py +++ b/copyparty/ico.py @@ -31,7 +31,7 @@ class Ico(object): w = 100 h = 30 - if not self.args.th_no_crop and as_thumb: + if "n" in self.args.th_crop and as_thumb: sw, sh = self.args.th_size.split("x") h = int(100.0 / (float(sw) / float(sh))) w = 100 diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 10ab1223..3a45aaf3 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -97,8 +97,8 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) - # spectrograms are never cropped; strip fullsize flag ext = rem.split(".")[-1].lower() - if ext in ffa and fmt in ("wf", "jf"): - fmt = fmt[:1] + if ext in ffa and fmt[:2] in ("wf", "jf"): + fmt = fmt.replace("f", "") rd += "\n" + fmt h = hashlib.sha512(afsenc(rd)).digest() @@ -200,9 +200,10 @@ class ThumbSrv(object): with self.mutex: return not self.nthr - def getres(self, vn: VFS) -> tuple[int, int]: + def getres(self, vn: VFS, fmt: str) -> tuple[int, int]: + mul = 3 if "3" in fmt else 1 w, h = vn.flags["thsize"].split("x") - return int(w), int(h) + return int(w) * mul, int(h) * mul def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]: histpath = self.asrv.vfs.histtab.get(ptop) @@ -364,7 +365,7 @@ class ThumbSrv(object): def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image": # exif_transpose is expensive (loads full image + unconditional copy) - res = self.getres(vn) + res = self.getres(vn, fmt) r = max(*res) * 2 im.thumbnail((r, r), resample=Image.LANCZOS) try: @@ -379,7 +380,7 @@ class ThumbSrv(object): if rot in rots: im = im.transpose(rots[rot]) - if fmt.endswith("f"): + if "f" in fmt: im.thumbnail(res, resample=Image.LANCZOS) else: iw, ih = im.size @@ -396,7 +397,7 @@ class ThumbSrv(object): im = self.fancy_pillow(im, fmt, vn) except Exception as ex: self.log("fancy_pillow {}".format(ex), "90") - im.thumbnail(self.getres(vn)) + im.thumbnail(self.getres(vn, fmt)) fmts = ["RGB", "L"] args = {"quality": 40} @@ -422,10 +423,10 @@ class ThumbSrv(object): def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) crops = ["centre", "none"] - if fmt.endswith("f"): + if "f" in fmt: crops = ["none"] - w, h = self.getres(vn) + w, h = self.getres(vn, fmt) kw = {"height": h, "size": "down", "intent": "relative"} for c in crops: @@ -454,12 +455,12 @@ class ThumbSrv(object): seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] scale = "scale={0}:{1}:force_original_aspect_ratio=" - if fmt.endswith("f"): + if "f" in fmt: scale += "decrease,setsar=1:1" else: scale += "increase,crop={0}:{1},setsar=1:1" - res = self.getres(vn) + res = self.getres(vn, fmt) bscale = scale.format(*list(res)).encode("utf-8") # fmt: off cmd = [ @@ -594,7 +595,11 @@ class ThumbSrv(object): need = 0.2 + dur / coeff self.wait4ram(need, tpath) - fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]" + fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" + if "3" in fmt: + fc += "1280x1024,crop=1420:1056:70:48[o]" + else: + fc += "640x512,crop=780:544:70:48[o]" if self.args.th_ff_swr: fco = ":filter_size=128:cutoff=0.877" diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2d49f450..7d71d34a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -349,7 +349,8 @@ var Ls = { "tvt_edit": "open file in text editor$NHotkey: E\">โœ๏ธ edit", "gt_msel": "enable file selection; ctrl-click a file to override$N$N<em>when active: doubleclick a file / folder to open it</em>$N$NHotkey: S\">multiselect", - "gt_full": "show uncropped thumbnails\">full", + "gt_crop": "center-crop thumbnails\">crop", + "gt_3x": "hi-res thumbnails\">3x", "gt_zoom": "zoom", "gt_chop": "chop", "gt_sort": "sort by", @@ -844,7 +845,8 @@ var Ls = { "tvt_edit": "redigรฉr filen$NSnarvei: E\">โœ๏ธ endre", "gt_msel": "markรฉr filer istedenfor รฅ รฅpne dem; ctrl-klikk filer for รฅ overstyre$N$N<em>nรฅr aktiv: dobbelklikk en fil / mappe for รฅ รฅpne</em>$N$NSnarvei: S\">markering", - "gt_full": "ikke beskjรฆr bildene\">full", + "gt_crop": "beskjรฆr ikonene sรฅ de passer bedre\">โœ‚", + "gt_3x": "hรธyere opplรธsning pรฅ ikoner\">3x", "gt_zoom": "zoom", "gt_chop": "trim", "gt_sort": "sorter", @@ -4515,7 +4517,9 @@ var thegrid = (function () { gfiles.innerHTML = ( '
' + ' ' + '+ ' + L.gt_chop + ': ' + ' ' + @@ -4530,7 +4534,7 @@ var thegrid = (function () { lfiles.parentNode.insertBefore(gfiles, lfiles); var r = { - 'sz': clamp(fcfg_get('gridsz', 10), 4, 40), + 'sz': clamp(fcfg_get('gridsz', 10), 4, 80), 'ln': clamp(icfg_get('gridln', 3), 1, 7), 'isdirty': true, 'bbox': null @@ -4593,10 +4597,10 @@ var thegrid = (function () { r.setdirty = function () { r.dirty = true; - if (r.en) { + if (r.en) loadgrid(); - } - r.setvis(); + else + r.setvis(); }; function setln(v) { @@ -4616,7 +4620,7 @@ var thegrid = (function () { function setsz(v) { if (v !== undefined) { - r.sz = clamp(v, 4, 40); + r.sz = clamp(v, 4, 80); swrite('gridsz', r.sz); setTimeout(r.tippen, 20); } @@ -4624,6 +4628,7 @@ var thegrid = (function () { document.documentElement.style.setProperty('--grid-sz', r.sz + 'em'); } catch (ex) { } + aligngriditems(); } setsz(); @@ -4776,8 +4781,11 @@ var thegrid = (function () { if (!r.dirty) return r.loadsel(); - if (dfull != r.full && !sread('gridfull')) - bcfg_upd_ui('gridfull', r.full = dfull); + if (dcrop.startsWith('f') || !sread('gridcrop')) + bcfg_upd_ui('gridcrop', r.crop = ('y' == dcrop.slice(-1))); + + if (dth3x.startsWith('f') || !sread('grid3x')) + bcfg_upd_ui('grid3x', r.x3 = ('y' == dth3x.slice(-1))); var html = [], svgs = new Set(), @@ -4796,8 +4804,10 @@ var thegrid = (function () { if (r.thumbs) { ihref += '?th=' + (have_webp ? 'w' : 'j'); - if (r.full) - ihref += 'f' + if (!r.crop) + ihref += 'f'; + if (r.x3) + ihref += '3'; if (href == "#") ihref = SR + '/.cpr/ico/' + (ref == 'moar' ? '++' : 'exit'); } @@ -4833,7 +4843,7 @@ var thegrid = (function () { html.push('' + ao.innerHTML + ''); } ebi('ggrid').innerHTML = html.join('\n'); @@ -4884,8 +4894,29 @@ var thegrid = (function () { })[0]; }; + r.set_crop = function (en) { + if (!dcrop.startsWith('f')) + return r.setdirty(); + + r.crop = dcrop.startsWith('y'); + bcfg_upd_ui('gridcrop', r.crop); + if (r.crop != en) + toast.warn(10, L.ul_btnlk); + }; + + r.set_x3 = function (en) { + if (!dth3x.startsWith('f')) + return r.setdirty(); + + r.x3 = dth3x.startsWith('y'); + bcfg_upd_ui('grid3x', r.x3); + if (r.x3 != en) + toast.warn(10, L.ul_btnlk); + }; + bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); - bcfg_bind(r, 'full', 'gridfull', false, r.setdirty); + bcfg_bind(r, 'crop', 'gridcrop', !dcrop.endsWith('n'), r.set_crop); + bcfg_bind(r, 'x3', 'grid3x', dth3x.endsWith('y'), r.set_x3); bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel); bcfg_bind(r, 'en', 'griden', dgrid, function (v) { v ? loadgrid() : r.setvis(true); @@ -5575,11 +5606,15 @@ function aligngriditems() { if (/b/.test(themen + '')) totalgapwidth *= 2.8; + var val, st = ebi('ggrid').style; + if (((griditemcount * em2px) * gridsz) + totalgapwidth < gridwidth) { - ebi('ggrid').style.justifyContent = 'left'; + val = 'left'; } else { - ebi('ggrid').style.justifyContent = treectl.hidden ? 'center' : 'space-between'; + val = treectl.hidden ? 'center' : 'space-between'; } + if (st.justifyContent != val) + st.justifyContent = val; } onresize100.add(aligngriditems); @@ -6110,7 +6145,8 @@ var treectl = (function () { res.files[a].tags = {}; read_dsort(res.dsort); - dfull = res.dfull; + dcrop = res.dcrop; + dth3x = res.dth3x; srvinf = res.srvinf; try { diff --git a/tests/util.py b/tests/util.py index 3f0967ee..25a9c03b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -110,7 +110,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats th_no_crop vague_403 vc ver xdev xlink xvol" + ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats th_x3 vague_403 vc ver xdev xlink xvol" ka.update(**{k: False for k in ex.split()}) ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip" @@ -156,7 +156,9 @@ class Cfg(Namespace): s_wr_sz=512 * 1024, sort="href", srch_hits=99999, + th_crop="y", th_size="320x256", + th_x3="n", u2sort="s", u2ts="c", unpost=600, From bbc379906aeb31a85902c11bf28adabb7c958652 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 14:11:01 +0000 Subject: [PATCH 20/36] jump to last viewed pic on viewer close --- copyparty/web/baguettebox.js | 9 +++++-- copyparty/web/browser.css | 7 ++++++ copyparty/web/browser.js | 49 ++++++++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index 39a9c8d1..cc5b2674 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -17,8 +17,10 @@ window.baguetteBox = (function () { titleTag: false, async: false, preload: 2, + refocus: true, afterShow: null, afterHide: null, + duringHide: null, onChange: null, }, overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose, @@ -144,7 +146,7 @@ window.baguetteBox = (function () { selectorData.galleries.push(gallery); }); - return selectorData.galleries; + return [selectorData.galleries, options]; } function clearCachedData() { @@ -593,6 +595,9 @@ window.baguetteBox = (function () { if (overlay.style.display === 'none') return; + if (options.duringHide) + options.duringHide(); + sethash(''); unbindEvents(); try { @@ -613,7 +618,7 @@ window.baguetteBox = (function () { if (options.afterHide) options.afterHide(); - documentLastFocus && documentLastFocus.focus(); + options.refocus && documentLastFocus && documentLastFocus.focus(); isOverlayVisible = false; unvid(); unfig(); diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 737e0a0f..8f35960c 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1247,6 +1247,13 @@ html.y #widget.open { 0% {opacity:0} 100% {opacity:1} } +#ggrid>a.glow { + animation: gexit .6s ease-out; +} +@keyframes gexit { + 0% {box-shadow: 0 0 0 2em var(--a)} + 100% {box-shadow: 0 0 0em 0em var(--a)} +} #wzip a { font-size: .4em; margin: -.3em .1em; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 7d71d34a..0a8f4a78 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -194,6 +194,7 @@ var Ls = { "ct_thumb": "in grid-view, toggle icons or thumbnails$NHotkey: T", "ct_csel": "use CTRL and SHIFT for file selection in grid-view", + "ct_ihop": "when the image viewer is closed, scroll down to the last viewed file", "ct_dots": "show hidden files (if server permits)", "ct_dir1st": "sort folders before files", "ct_readme": "show README.md in folder listings", @@ -690,6 +691,7 @@ var Ls = { "ct_thumb": "vis miniatyrbilder istedenfor ikoner$NSnarvei: T", "ct_csel": "bruk tastene CTRL og SHIFT for markering av filer i ikonvisning", + "ct_ihop": "bla ned til sist viste bilde nรฅr bildeviseren lukkes", "ct_dots": "vis skjulte filer (gitt at serveren tillater det)", "ct_dir1st": "sorter slik at mapper kommer foran filer", "ct_readme": "vis README.md nedenfor filene", @@ -1185,6 +1187,7 @@ ebi('op_cfg').innerHTML = ( ' ็”ฐ the grid\n' + ' ๐Ÿ–ผ๏ธ thumbs\n' + ' sel\n' + + ' gโฎฏ\n' + ' dotfiles\n' + ' ๐Ÿ“ first\n' + ' ๐Ÿ“œ readme\n' + @@ -4875,7 +4878,11 @@ var thegrid = (function () { if (r.bbox) baguetteBox.destroy(); - r.bbox = baguetteBox.run(isrc, { + var br = baguetteBox.run(isrc, { + duringHide: r.onhide, + afterShow: function () { + r.bbox_opts.refocus = true; + }, captions: function (g) { var idx = -1, h = '' + g; @@ -4891,7 +4898,44 @@ var thegrid = (function () { onChange: function (i) { sethash('g' + r.bbox[i].imageElement.getAttribute('ref')); } - })[0]; + }); + r.bbox = br[0][0]; + r.bbox_opts = br[1]; + }; + + r.onhide = function () { + if (!thegrid.ihop) + return; + + try { + var el = QS('#ggrid a[ref="' + location.hash.slice(2) + '"]'), + f = function () { + try { + el.focus(); + } + catch (ex) { } + }; + + f(); + setTimeout(f, 10); + setTimeout(f, 100); + setTimeout(f, 200); + // thx fullscreen api + + if (ANIM) { + clmod(el, 'glow', 1); + setTimeout(function () { + try { + clmod(el, 'glow'); + } + catch (ex) { } + }, 600); + } + r.bbox_opts.refocus = false; + } + catch (ex) { + console.log('ihop:', ex); + } }; r.set_crop = function (en) { @@ -4915,6 +4959,7 @@ var thegrid = (function () { }; bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); + bcfg_bind(r, 'ihop', 'ihop', true); bcfg_bind(r, 'crop', 'gridcrop', !dcrop.endsWith('n'), r.set_crop); bcfg_bind(r, 'x3', 'grid3x', dth3x.endsWith('y'), r.set_x3); bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel); From fbfdd8338b0dd5cdfbb59da17a4ff94c459e7241 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 14:11:48 +0000 Subject: [PATCH 21/36] respect `prefers-reduced-motion` some more places --- copyparty/web/browser.css | 24 +++++++++++++++++++++--- copyparty/web/ui.css | 8 ++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 8f35960c..18ae6a16 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1151,9 +1151,6 @@ html.y #widget.open { @keyframes spin { 100% {transform: rotate(360deg)} } -@media (prefers-reduced-motion) { - @keyframes spin { } -} @keyframes fadein { 0% {opacity: 0} 100% {opacity: 1} @@ -3170,3 +3167,24 @@ html.d #treepar { padding: 0.2em; } } + + + + + +@media (prefers-reduced-motion) { + @keyframes spin { } + @keyframes gexit { } + @keyframes bounce { } + @keyframes bounceFromLeft { } + @keyframes bounceFromRight { } + + #ggrid>a:before, + #widget.anim, + #u2tabw, + .dropdesc, + .dropdesc b, + .dropdesc>div>div { + transition: none; + } +} diff --git a/copyparty/web/ui.css b/copyparty/web/ui.css index e27030ec..ffed63e3 100644 --- a/copyparty/web/ui.css +++ b/copyparty/web/ui.css @@ -580,3 +580,11 @@ hr { border: .07em dashed #444; } } + +@media (prefers-reduced-motion) { + #toast, + #toast a#toastc, + #tt { + transition: none; + } +} \ No newline at end of file From 7f1c9926013efc66ee7b2788473447e5ed68dae7 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 14:50:59 +0000 Subject: [PATCH 22/36] prevent scrolling while gallery is open + firefox52/winxp: fix gridview margins --- copyparty/web/browser.css | 2 +- copyparty/web/browser.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 18ae6a16..54315bb9 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -3142,7 +3142,7 @@ html.d #treepar { margin-top: 1.7em; } } -@supports (display: grid) { +@supports (display: grid) and (gap: 1em) { #ggrid { display: grid; margin: 0em 0.25em; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 0a8f4a78..f6fbd598 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4882,6 +4882,7 @@ var thegrid = (function () { duringHide: r.onhide, afterShow: function () { r.bbox_opts.refocus = true; + document.body.style.overflow = 'hidden'; }, captions: function (g) { var idx = -1, @@ -4904,6 +4905,7 @@ var thegrid = (function () { }; r.onhide = function () { + document.body.style.overflow = ''; if (!thegrid.ihop) return; From 58ae38c6134a6a6b440df27e45366bf9c1509dcd Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 15:36:59 +0000 Subject: [PATCH 23/36] enforce thumbnail config serverside --- copyparty/th_cli.py | 36 +++++++++++++++++++++++++++--------- copyparty/web/browser.js | 4 ++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 296527f4..9cfef9aa 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -78,16 +78,34 @@ class ThumbCli(object): if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]: return os.path.join(ptop, rem) - if fmt == "j" and self.args.th_no_jpg: - fmt = "w" + if fmt[:1] in "jw": + sfmt = fmt[:1] - if fmt == "w": - if ( - self.args.th_no_webp - or (is_img and not self.can_webp) - or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) - ): - fmt = "j" + if sfmt == "j" and self.args.th_no_jpg: + sfmt = "w" + + if sfmt == "w": + if ( + self.args.th_no_webp + or (is_img and not self.can_webp) + or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) + ): + sfmt = "j" + + vf_crop = dbv.flags["crop"] + vf_th3x = dbv.flags["th3x"] + + if "f" in vf_crop: + sfmt += "f" if "n" in vf_crop else "" + else: + sfmt += "f" if "f" in fmt else "" + + if "f" in vf_th3x: + sfmt += "3" if "y" in vf_th3x else "" + else: + sfmt += "3" if "3" in fmt else "" + + fmt = sfmt histpath = self.asrv.vfs.histtab.get(ptop) if not histpath: diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f6fbd598..f4f0e05e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4944,7 +4944,7 @@ var thegrid = (function () { if (!dcrop.startsWith('f')) return r.setdirty(); - r.crop = dcrop.startsWith('y'); + r.crop = dcrop.endsWith('y'); bcfg_upd_ui('gridcrop', r.crop); if (r.crop != en) toast.warn(10, L.ul_btnlk); @@ -4954,7 +4954,7 @@ var thegrid = (function () { if (!dth3x.startsWith('f')) return r.setdirty(); - r.x3 = dth3x.startsWith('y'); + r.x3 = dth3x.endsWith('y'); bcfg_upd_ui('grid3x', r.x3); if (r.x3 != en) toast.warn(10, L.ul_btnlk); From 8ff7094e4dea3d225071cf37bc0b64a85c69bf83 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 15:44:54 +0000 Subject: [PATCH 24/36] fix sharex config example --- contrib/README.md | 5 ----- contrib/sharex-html.sxcu | 19 ------------------- contrib/sharex.sxcu | 8 +++++--- 3 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 contrib/sharex-html.sxcu diff --git a/contrib/README.md b/contrib/README.md index 50627b09..1dab4824 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -17,11 +17,6 @@ * `RequestURL`: full URL to the target folder * `pw`: password (remove the `pw` line if anon-write) -however if your copyparty is behind a reverse-proxy, you may want to use [`sharex-html.sxcu`](sharex-html.sxcu) instead: -* `RequestURL`: full URL to the target folder -* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$` -* `pw`: password (remove `Parameters` if anon-write) - ### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json) * browser integration, kind of? custom rightclick actions and stuff * rightclick a pic and send it to copyparty straight from your browser diff --git a/contrib/sharex-html.sxcu b/contrib/sharex-html.sxcu deleted file mode 100644 index 6d8dce06..00000000 --- a/contrib/sharex-html.sxcu +++ /dev/null @@ -1,19 +0,0 @@ -{ - "Version": "13.5.0", - "Name": "copyparty-html", - "DestinationType": "ImageUploader", - "RequestMethod": "POST", - "RequestURL": "http://127.0.0.1:3923/sharex", - "Parameters": { - "pw": "wark" - }, - "Body": "MultipartFormData", - "Arguments": { - "act": "bput" - }, - "FileFormName": "f", - "RegexList": [ - "bytes // Date: Sun, 18 Feb 2024 15:54:38 +0000 Subject: [PATCH 25/36] v1.10.1 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 32 ++++++++++++++++++++++++++++++++ tests/util.py | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 8a29f469..4b748672 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 10, 0) +VERSION = (1, 10, 1) CODENAME = "tftp" -BUILD_DT = (2024, 2, 15) +BUILD_DT = (2024, 2, 18) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index ba035215..c960a29d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,35 @@ +โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ +# 2024-0215-0000 `v1.10.0` tftp + +## new features + +* TFTP server d636316a 8796c09f acbb8267 02879713 + * based on [partftpy](https://github.com/9001/partftpy), has most essential features EXCEPT for [rfc7440](https://datatracker.ietf.org/doc/html/rfc7440) so WAN will be slow + * is already doing real work out in the wild! see the fantastic quote in the [readme](https://github.com/9001/copyparty?tab=readme-ov-file#tftp-server) +* detect some (un)common configuration mistakes + * buggy reverse-proxy which strips away all URL parameters 136c0fdc + * could cause the browser to get stuck in a refresh-loop + * a volume on an sqlite-incompatible filesystem (a remote cifs server or such) and an up2k volume inside d4da3861 + * sqlite could deadlock or randomly throw exceptions; serverlog will now explain how to fix it +* ie11: file selection with shift-up/down 64ad5853 + +## bugfixes + +* prevent music playback from stopping at the end of a folder f262aee8 + * preloader will now proactively hunt for the next file to play as the last song is ending +* in very specific scenarios, clients could be told their upload had finished processing a tiny bit too early, while the HDD was still busy taking in the last couple bytes 6f8a588c + * so if you expected to find the complete file on the server HDD immediately as the final chunk got confirmed, that was not necessarily the case if your server HDD was severely overloaded to the point where closing a file takes half a minute + * huge thx to friend with said overloaded server for finding all the crazy edge cases +* ignore harmless javascript errors from easymde 879e83e2 + +## other changes + +* the "copy currently playing song info to clipboard" button now excludes the uploader IP ed524d84 +* mention that enabling `-j0` can improve HDD load during uploads 5d92f4df +* mention a debian-specific docker bug which prevents starting most containers (not just copyparty) 4e797a71 + + + โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ # 2024-0203-1533 `v1.9.31` eject diff --git a/tests/util.py b/tests/util.py index 25a9c03b..c64f39a5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -110,7 +110,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats th_x3 vague_403 vc ver xdev xlink xvol" + ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats vague_403 vc ver xdev xlink xvol" ka.update(**{k: False for k in ex.split()}) ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip" From ad9be54f55172e791e06a289dda08df7eb69cdc0 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 16:17:28 +0000 Subject: [PATCH 26/36] update pkgs to 1.10.1 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index c28957b1..b60c4034 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: icxes pkgname=copyparty -pkgver="1.10.0" +pkgver="1.10.1" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, 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=("a44338b24d28fd6962504ec2c0e466e16144ddd4c5af44eb3ae493534152fd07") +sha256sums=("3969bbacccaa2fbb4c0bb1c971d9fd7d1851c35f829a1f2f02ad281f5f6dfe53") build() { cd "${srcdir}/${pkgname}-${pkgver}" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index fc4f34a1..c7955ea4 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.10.0/copyparty-sfx.py", - "version": "1.10.0", - "hash": "sha256-pPkiDKXEv7P1zPB7/BSo85St0FNnhUpb30sxIi7mn1c=" + "url": "https://github.com/9001/copyparty/releases/download/v1.10.1/copyparty-sfx.py", + "version": "1.10.1", + "hash": "sha256-p1SF0BKY+qcs+/ZpqgU3dfK4E+/rpxezsiY6U1obhx4=" } \ No newline at end of file From df7219d3b640455faddc5699a96647c5d73ac300 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 19 Feb 2024 19:42:39 +0000 Subject: [PATCH 27/36] cropping folder icons is dumb --- copyparty/ico.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/copyparty/ico.py b/copyparty/ico.py index 28537d25..1178f4a5 100644 --- a/copyparty/ico.py +++ b/copyparty/ico.py @@ -31,10 +31,9 @@ class Ico(object): w = 100 h = 30 - if "n" in self.args.th_crop and as_thumb: + if as_thumb: sw, sh = self.args.th_size.split("x") h = int(100.0 / (float(sw) / float(sh))) - w = 100 if chrome: # cannot handle more than ~2000 unique SVGs From d07859e8e6a8cae26ba11fc337cb5630bcfa4593 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 00:06:47 +0000 Subject: [PATCH 28/36] fix a handful of tftp crashes: * if a nic was restarted mid-transfer, the server could crash * this workaround will probably fix a bunch of similar issues too * fix resource leak if dualstack fails the ipv4 bind --- copyparty/ftpd.py | 3 +++ copyparty/tftpd.py | 62 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 2bc99c38..4d72c4b1 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -20,6 +20,7 @@ from .authsrv import VFS from .bos import bos from .util import ( Daemon, + ODict, Pebkac, exclude_dotfiles, fsenc, @@ -545,6 +546,8 @@ class Ftpd(object): if self.args.ftp4: ips = [x for x in ips if ":" not in x] + ips = list(ODict.fromkeys(ips)) # dedup + ioloop = IOLoop() for ip in ips: for h, lp in hs: diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index ac1aaedf..0020e96a 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -36,7 +36,7 @@ from partftpy.TftpShared import TftpException from .__init__ import EXE, TYPE_CHECKING from .authsrv import VFS from .bos import bos -from .util import BytesIO, Daemon, exclude_dotfiles, min_ex, runhook, undot +from .util import BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot if True: # pylint: disable=using-constant-test from typing import Any, Union @@ -169,6 +169,8 @@ class Tftpd(object): if self.args.ftp4: ips = [x for x in ips if ":" not in x] + ips = list(ODict.fromkeys(ips)) # dedup + for ip in ips: name = "tftp_%s" % (ip,) Daemon(self._start, name, [ip, ports]) @@ -179,18 +181,54 @@ class Tftpd(object): def _start(self, ip, ports): fam = socket.AF_INET6 if ":" in ip else socket.AF_INET - srv = TftpServer.TftpServer("/", self._ls) - with self.mutex: - self.srv.append(srv) - self.ips.append(ip) - try: - srv.listen(ip, self.port, af_family=fam, ports=ports) - except OSError: + have_been_alive = False + while True: + srv = TftpServer.TftpServer("/", self._ls) with self.mutex: - self.srv.remove(srv) - self.ips.remove(ip) - if ip != "0.0.0.0" or "::" not in self.ips: - raise + self.srv.append(srv) + self.ips.append(ip) + + try: + # this is the listen loop; it should block forever + srv.listen(ip, self.port, af_family=fam, ports=ports) + except: + with self.mutex: + self.srv.remove(srv) + self.ips.remove(ip) + + try: + srv.sock.close() + except: + pass + + try: + bound = bool(srv.listenport) + except: + bound = False + + if bound: + # this instance has managed to bind at least once + have_been_alive = True + + if have_been_alive: + t = "tftp server [%s]:%d crashed; restarting in 3 sec:\n%s" + error(t, ip, self.port, min_ex()) + time.sleep(3) + continue + + # server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4) + if ip != "0.0.0.0" or "::" not in self.ips: + # nope, it's fatal + t = "tftp server [%s]:%d failed to start:\n%s" + error(t, ip, self.port, min_ex()) + + # yep; ignore + # (TODO: move the "listening @ ..." infolog in partftpy to + # after the bind attempt so it doesn't print twice) + return + + info("tftp server [%s]:%d terminated", ip, self.port) + break def stop(self): with self.mutex: From 5026b2122678949974ec18cf02a5f4a56927aca6 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 08:27:03 +0000 Subject: [PATCH 29/36] gridview: uncropped tall pics are tall + more granular zoom --- copyparty/web/browser.css | 4 ++++ copyparty/web/browser.js | 35 ++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 54315bb9..ca31c7a3 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -985,6 +985,10 @@ html.y #path a:hover { margin: 0 auto; display: block; } +#ggrid.nocrop>a img { + max-height: 20em; + max-height: calc(var(--grid-sz)*2); +} #ggrid>a.dir:before { content: '๐Ÿ“‚'; } diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f4f0e05e..6102a866 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4523,8 +4523,8 @@ var thegrid = (function () { ' ' + - '+ ' + L.gt_chop + ': ' + + ' ' + + '+ ' + L.gt_chop + ': ' + ' ' + '+ ' + L.gt_sort + ': ' + '' + L.gt_name + ' ' + @@ -4535,6 +4535,7 @@ var thegrid = (function () { '
' ); lfiles.parentNode.insertBefore(gfiles, lfiles); + var ggrid = ebi('ggrid'); var r = { 'sz': clamp(fcfg_get('gridsz', 10), 4, 80), @@ -4555,7 +4556,7 @@ var thegrid = (function () { if (l) return setln(parseInt(l)); - var t = ebi('files').tHead.rows[0].cells; + var t = lfiles.tHead.rows[0].cells; for (var a = 0; a < t.length; a++) if (t[a].getAttribute('name') == s) { t[a].click(); @@ -4580,10 +4581,13 @@ var thegrid = (function () { lfiles = ebi('files'); gfiles = ebi('gfiles'); + ggrid = ebi('ggrid'); var vis = has(perms, "read"); gfiles.style.display = vis && r.en ? '' : 'none'; lfiles.style.display = vis && !r.en ? '' : 'none'; + clmod(ggrid, 'crop', r.crop); + clmod(ggrid, 'nocrop', !r.crop); ebi('pro').style.display = ebi('epi').style.display = ebi('lazy').style.display = ebi('treeul').style.display = ebi('treepar').style.display = ''; ebi('bdoc').style.display = 'none'; clmod(ebi('wrap'), 'doc'); @@ -4773,7 +4777,7 @@ var thegrid = (function () { pels[a].removeAttribute('tt'); } - tt.att(ebi('ggrid')); + tt.att(ggrid); }; function loadgrid() { @@ -4849,7 +4853,9 @@ var thegrid = (function () { (r.sz / 1.25) + 'em" loading="lazy" onload="th_onload(this)" src="' + ihref + '" />' + ao.innerHTML + '
'); } - ebi('ggrid').innerHTML = html.join('\n'); + ggrid.innerHTML = html.join('\n'); + clmod(ggrid, 'crop', r.crop); + clmod(ggrid, 'nocrop', !r.crop); var srch = ebi('unsearch'), gsel = ebi('gridsel'); @@ -5641,19 +5647,21 @@ function aligngriditems() { if (!treectl) return; - var em2px = parseFloat(getComputedStyle(ebi('ggrid')).fontSize); - var gridsz = 10; + var ggrid = ebi('ggrid'), + em2px = parseFloat(getComputedStyle(ggrid).fontSize), + gridsz = 10; try { gridsz = cprop('--grid-sz').slice(0, -2); } catch (ex) { } - var gridwidth = ebi('ggrid').clientWidth; - var griditemcount = ebi('ggrid').children.length; - var totalgapwidth = em2px * griditemcount; + var gridwidth = ggrid.clientWidth, + griditemcount = ggrid.children.length, + totalgapwidth = em2px * griditemcount; + if (/b/.test(themen + '')) totalgapwidth *= 2.8; - var val, st = ebi('ggrid').style; + var val, st = ggrid.style; if (((griditemcount * em2px) * gridsz) + totalgapwidth < gridwidth) { val = 'left'; @@ -6166,13 +6174,14 @@ var treectl = (function () { r.nextdir = null; var cdir = get_evpath(), - cur = ebi('files').getAttribute('ts'); + lfiles = ebi('files'), + cur = lfiles.getAttribute('ts'); if (cur && parseInt(cur) > this.ts) { console.log("reject ls"); return; } - ebi('files').setAttribute('ts', this.ts); + lfiles.setAttribute('ts', this.ts); try { var res = JSON.parse(this.responseText); From 8a38101e4858abbb54e4e43d1550256530bf79ac Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 08:39:23 +0000 Subject: [PATCH 30/36] return icon that says 403/404 if file inaccessible --- copyparty/httpcli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index fc12be4f..12a92e91 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3139,7 +3139,7 @@ class HttpCli(object): ext = ext.rstrip(".") or "unk" if len(ext) > 11: - ext = "โ‹ฏ" + ext[-9:] + ext = "~" + ext[-9:] # chrome cannot handle more than ~2000 unique SVGs chrome = " rv:" not in self.ua @@ -3407,6 +3407,9 @@ class HttpCli(object): self.reply(pt.encode("utf-8"), status=rc) return True + if "th" in self.ouparam: + return self.tx_ico("a.e" + pt[:3]) + t = t.format(self.args.SR) qv = quotep(self.vpaths) + self.ourlq() html = self.j2s("splash", this=self, qvpath=qv, msg=t) From 43ee6b9f5bfaaaff18c08ad6e084108997e22eb5 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 18:44:56 +0000 Subject: [PATCH 31/36] stop cloudflare from jumbling up png/svg icons; chrome crashes if there's more than 2000 unique SVGs on one page, so there was serverside useragent-sniffing to determine if the icon should be an svg or a raster however since the useragent is not in our vary, cloudflare wouldn't see the difference and cache everything equally, meaning most folders would display a random mix of png and svg thumbnails move browser detection to the clientside to ensure unique URLs --- copyparty/httpcli.py | 5 +++-- copyparty/web/browser.js | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 12a92e91..643c7cf5 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3142,8 +3142,9 @@ class HttpCli(object): ext = "~" + ext[-9:] # chrome cannot handle more than ~2000 unique SVGs - chrome = " rv:" not in self.ua - mime, ico = self.ico.get(ext, not exact, chrome) + # so url-param "raster" returns a png/webp instead + # (useragent-sniffing kinshi due to caching proxies) + mime, ico = self.ico.get(ext, not exact, "raster" in self.uparam) lm = formatdate(self.E.t0, usegmt=True) self.reply(ico, mime=mime, headers={"Last-Modified": lm}) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 6102a866..03fb2339 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4847,6 +4847,8 @@ var thegrid = (function () { ihref = SR + '/.cpr/ico/' + ext; } ihref += (ihref.indexOf('?') > 0 ? '&' : '?') + 'cache=i&_=' + ACB; + if (CHROME) + ihref += "&raster"; html.push(' {%- endif %} + diff --git a/copyparty/web/mde.html b/copyparty/web/mde.html index 8b57336b..d9b3c056 100644 --- a/copyparty/web/mde.html +++ b/copyparty/web/mde.html @@ -54,3 +54,4 @@ try { l.light = drk? 0:1; } catch (ex) { } + diff --git a/copyparty/web/msg.html b/copyparty/web/msg.html index 72025919..4ef5973a 100644 --- a/copyparty/web/msg.html +++ b/copyparty/web/msg.html @@ -48,4 +48,5 @@ {%- endif %} - \ No newline at end of file + + diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 29776e19..79391610 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -118,3 +118,4 @@ document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme + diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 1353ffe6..49ca9a02 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -246,3 +246,4 @@ document.documentElement.className = (STG && STG.cpp_thm) || "{{ args.theme }}"; + From 14af136fcdff8e5dcfe09ef0368e39fc7c25ca2f Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 19:19:30 +0000 Subject: [PATCH 33/36] force generic "folder" icon when image-thumbs are disabled fixes the "unk" that would be shown if a subfolder contains images --- copyparty/httpcli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 643c7cf5..6c9f1698 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3794,9 +3794,12 @@ class HttpCli(object): if self.can_read: th_fmt = self.uparam.get("th") if th_fmt is not None: + nothumb = "dthumb" in dbv.flags if is_dir: vrem = vrem.rstrip("/") - if icur and vrem: + if nothumb: + pass + elif icur and vrem: q = "select fn from cv where rd=? and dn=?" crd, cdn = vrem.rsplit("/", 1) if "/" in vrem else ("", vrem) # no mojibake support: @@ -3822,7 +3825,7 @@ class HttpCli(object): return self.tx_ico("a.folder") thp = None - if self.thumbcli: + if self.thumbcli and not nothumb: thp = self.thumbcli.get(dbv, vrem, int(st.st_mtime), th_fmt) if thp: From 89c6c2e0d9de6b47a8bbfcadc8551bc55e03ac72 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 20:57:18 +0000 Subject: [PATCH 34/36] "upload only" icon on write-only folders --- copyparty/httpcli.py | 14 ++++++++++---- copyparty/ico.py | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6c9f1698..12b0f68e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3141,10 +3141,13 @@ class HttpCli(object): if len(ext) > 11: ext = "~" + ext[-9:] + return self.tx_svg(ext, exact) + + def tx_svg(self, txt: str, small: bool = False) -> bool: # chrome cannot handle more than ~2000 unique SVGs # so url-param "raster" returns a png/webp instead # (useragent-sniffing kinshi due to caching proxies) - mime, ico = self.ico.get(ext, not exact, "raster" in self.uparam) + mime, ico = self.ico.get(txt, not small, "raster" in self.uparam) lm = formatdate(self.E.t0, usegmt=True) self.reply(ico, mime=mime, headers={"Last-Modified": lm}) @@ -3409,7 +3412,7 @@ class HttpCli(object): return True if "th" in self.ouparam: - return self.tx_ico("a.e" + pt[:3]) + return self.tx_svg("e" + pt[:3]) t = t.format(self.args.SR) qv = quotep(self.vpaths) + self.ourlq() @@ -3791,8 +3794,8 @@ class HttpCli(object): if idx and hasattr(idx, "p_end"): icur = idx.get_cur(dbv.realpath) + th_fmt = self.uparam.get("th") if self.can_read: - th_fmt = self.uparam.get("th") if th_fmt is not None: nothumb = "dthumb" in dbv.flags if is_dir: @@ -3822,7 +3825,7 @@ class HttpCli(object): break if is_dir: - return self.tx_ico("a.folder") + return self.tx_svg("folder") thp = None if self.thumbcli and not nothumb: @@ -3836,6 +3839,9 @@ class HttpCli(object): return self.tx_ico(rem) + elif self.can_write and th_fmt is not None: + return self.tx_svg("upload\nonly") + elif self.can_get and self.avn: axs = self.avn.axs if self.uname not in axs.uhtml: diff --git a/copyparty/ico.py b/copyparty/ico.py index 1178f4a5..9788a0b6 100644 --- a/copyparty/ico.py +++ b/copyparty/ico.py @@ -8,7 +8,7 @@ import re from .__init__ import PY2 from .th_srv import HAVE_PIL, HAVE_PILF -from .util import BytesIO # type: ignore +from .util import BytesIO, html_escape # type: ignore class Ico(object): @@ -98,6 +98,6 @@ class Ico(object): fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{} """ - svg = svg.format(h, c[:6], c[6:], ext) + svg = svg.format(h, c[:6], c[6:], html_escape(ext, True)) return "image/svg+xml", svg.encode("utf-8") From 13e77777d74ebfcd65a0efae29ec4f2a0d6ec39c Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 21:32:11 +0000 Subject: [PATCH 35/36] v1.10.2 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 33 +++++++++++++++++++++++++++++++++ scripts/deps-docker/Dockerfile | 2 +- scripts/pyinstaller/deps.sha512 | 13 +++++++------ scripts/pyinstaller/notes.txt | 11 ++++++----- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 4b748672..73e66913 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 10, 1) +VERSION = (1, 10, 2) CODENAME = "tftp" -BUILD_DT = (2024, 2, 18) +BUILD_DT = (2024, 2, 21) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index c960a29d..b8fa417e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,36 @@ +โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ +# 2024-0218-1554 `v1.10.1` big thumbs + +## new features + +* button to enable hi-res thumbnails 33f41f3e 58ae38c6 + * enable with the `3x` button in the gridview + * can be force-enabled/disabled serverside with `--th-x3` or volflag `th3x` +* tftp: IPv6 support and UTF-8 filenames + optimizations 0504b010 +* ux: + * when closing the image viewer, scroll to the last viewed pic bbc37990 + * respect `prefers-reduced-motion` some more places fbfdd833 + +## bugfixes + +* #72 impossible to delete recently uploaded zerobyte files if database was disabled 6bd087dd +* tftp now works in `copyparty.exe`, `copyparty32.exe`, `copyparty-winpe64.exe` +* the [sharex config example](https://github.com/9001/copyparty/tree/hovudstraum/contrib#sharexsxcu) was still using cookie-auth 8ff7094e +* ux: + * prevent scrolling while a pic is open 7f1c9926 + * fix gridview in older firefox versions 7f1c9926 + +## other changes + +* thumbnail center-cropping can be force-enabled/disabled serverside with `--th-crop` or volflag `crop` + * replaces `--th-no-crop` which is now deprecated (but will continue to work) + +---- + +this release contains a build of `copyparty-winpe64.exe` which is almost **entirely useless,** except for in *extremely specific scenarios*, namely the kind where a TFTP server could also be useful -- the [previous build](https://github.com/9001/copyparty/releases/download/v1.8.7/copyparty-winpe64.exe) was from [version 1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23) + + + โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€ # 2024-0215-0000 `v1.10.0` tftp diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index cc187bd9..96c08e88 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /z ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \ ver_hashwasm=4.10.0 \ ver_marked=4.3.0 \ - ver_dompf=3.0.8 \ + ver_dompf=3.0.9 \ ver_mde=2.18.0 \ ver_codemirror=5.65.16 \ ver_fontawesome=5.13.0 \ diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index de7912b1..77ecf208 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -1,13 +1,10 @@ f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl eda6c38fc4d813fee897e969ff9ecc5acc613df755ae63df0392217bbd67408b5c1f6c676f2bf5497b772a3eb4e1a360e1245e1c16ee83f0af555f1ab82c3977 Git-2.39.1-32-bit.exe 17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl -f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61 pyinstaller-5.13.2-py3-none-win32.whl -f23615c522ed58b9a05978ba4c69c06224590f3a6adbd8e89b31838b181a57160739ceff1fc2ba6f4239b8fee46f92ce02910b2debda2710558ed42cff1ce3f1 pyinstaller-6.1.0-py3-none-win_amd64.whl -5747b3b119629c4cf956f0eaa85f29218bb3680d3a4a262fa6e976e56b35067302e153d2c0a001505f2cb642b1f78752567889b3b82e342d6cd29aac8b70e92e pyinstaller_hooks_contrib-2023.10-py2.py3-none-any.whl +f042aabe6cca2ae368180eaf313dd58f9ee96384c0ac1064aefe24a9e0e7e9cd6efa74eacb125d51a8feb61eaf200bc84812ab4d90c08fe33ef315eb2d9e6c30 pyinstaller_hooks_contrib-2024.1-py2.py3-none-any.whl 749a473646c6d4c7939989649733d4c7699fd1c359c27046bf5bc9c070d1a4b8b986bbc65f60d7da725baf16dbfdd75a4c2f5bb8335f2cb5685073f5fee5c2d1 pywin32_ctypes-0.2.2-py3-none-any.whl 6e0d854040baff861e1647d2bece7d090bc793b2bd9819c56105b94090df54881a6a9b43ebd82578cd7c76d47181571b671e60672afd9def389d03c9dae84fcf setuptools-68.2.2-py3-none-any.whl 3c5adf0a36516d284a2ede363051edc1bcc9df925c5a8a9fa2e03cab579dd8d847fdad42f7fd5ba35992e08234c97d2dbfec40a9d12eec61c8dc03758f2bd88e typing_extensions-4.4.0-py3-none-any.whl -8d16a967a0a7872a7575b1005cf66915deacda6ee8611fbb52f42fc3e3beb2f901a5140c942a5d146bd412b92bfa9cbadd82beeba83df6d70930c6dc26608a5b upx-4.1.0-win32.zip # u2c (win7) f3390290b896019b2fa169932390e4930d1c03c014e1f6db2405ca2eb1f51f5f5213f725885853805b742997b0edb369787e5c0069d217bc4e8b957f847f58b6 certifi-2023.11.17-py3-none-any.whl 904eb57b13bea80aea861de86987e618665d37fa9ea0856e0125a9ba767a53e5064de0b9c4735435a2ddf4f16f7f7d2c75a682e1de83d9f57922bdca8e29988c charset_normalizer-3.3.0-cp37-cp37m-win32.whl @@ -18,15 +15,19 @@ b795abb26ba2f04f1afcfb196f21f638014b26c8186f8f488f1c2d91e8e0220962fbd259dbc9c387 91c025f7d94bcdf93df838fab67053165a414fc84e8496f92ecbb910dd55f6b6af5e360bbd051444066880c5a6877e75157bd95e150ead46e5c605930dfc50f2 future-0.18.2.tar.gz c06b3295d1d0b0f0a6f9a6cd0be861b9b643b4a5ea37857f0bd41c45deaf27bb927b71922dab74e633e43d75d04a9bd0d1c4ad875569740b0f2a98dd2bfa5113 importlib_metadata-5.0.0-py3-none-any.whl 016a8cbd09384f1a9a44cb0e8274df75a8bcb2f3966bb5d708c62145289efaa5db98f75256c97e4f8046735ce2e529fbb076f284a46cdb716e89a75660200ad9 pip-23.2.1-py3-none-any.whl +f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61 pyinstaller-5.13.2-py3-none-win32.whl 6bb73cc2db795c59c92f2115727f5c173cacc9465af7710db9ff2f2aec2d73130d0992d0f16dcb3fac222dc15c0916562d0813b2337401022020673a4461df3d python-3.7.9-amd64.exe 500747651c87f59f2436c5ab91207b5b657856e43d10083f3ce27efb196a2580fadd199a4209519b409920c562aaaa7dcbdfb83ed2072a43eaccae6e2d056f31 python-3.7.9.exe +2e04acff170ca3bbceeeb18489c687126c951ec0bfd53cccfb389ba8d29a4576c1a9e8f2e5ea26c84dd21bfa2912f4e71fa72c1e2653b71e34afc0e65f1722d4 upx-4.2.2-win32.zip 68e1b618d988be56aaae4e2eb92bc0093627a00441c1074ebe680c41aa98a6161e52733ad0c59888c643a33fe56884e4f935178b2557fbbdd105e92e0d993df6 windows6.1-kb2533623-x64.msu 479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361 windows6.1-kb2533623-x86.msu ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a25067512b8f75538c67c987cf3944bfa0229e3cb677e2fb81e763e zipp-3.10.0-py3-none-any.whl # win10 -00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl -7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl +e3e2e6bd511dec484dd0292f4c46c55c88a885eabf15413d53edea2dd4a4dbae1571735b9424f78c0cd7f1082476a8259f31fd3f63990f726175470f636df2b3 Jinja2-3.1.3-py3-none-any.whl +e21495f1d473d855103fb4a243095b498ec90eb68776b0f9b48e994990534f7286c0292448e129c507e5d70409f8a05cca58b98d59ce2a815993d0a873dfc480 MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl 8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl 656015f5cc2c04aa0653ee5609c39a7e5f0b6a58c84fe26b20bd070c52d20b4effb810132f7fb771168483e9fd975cc3302837dd7a1a687ee058b0460c857cc4 packaging-23.2-py3-none-any.whl 424e20dc7263a31d524307bc39ed755a9dd82f538086fff68d98dd97e236c9b00777a8ac2e3853081b532b0e93cef44983e74d0ab274877440e8b7341b19358a pillow-10.2.0-cp311-cp311-win_amd64.whl +533b1aec21439032cf13084d84c4d862e41835a0468f34fef36c5d7cb9cf106a030826ac2e95c9e860f623f6a55ea58548f749c31594f388207d0809dc0859b5 pyinstaller-6.4.0-py3-none-win_amd64.whl e6bdbae1affd161e62fc87407c912462dfe875f535ba9f344d0c4ade13715c947cd3ae832eff60f1bad4161938311d06ac8bc9b52ef203f7b0d9de1409f052a5 python-3.11.8-amd64.exe +729dc52f1a02bc6274d012ce33f534102975a828cba11f6029600ea40e2d23aefeb07bf4ae19f9621d0565dd03eb2635bbb97d45fb692c1f756315e8c86c5255 upx-4.2.2-win64.zip diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 91904192..d9e98c0f 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -17,19 +17,19 @@ uname -s | grep NT-10 && w10=1 || { fns=( altgraph-0.17.4-py2.py3-none-any.whl pefile-2023.2.7-py3-none-any.whl - pyinstaller_hooks_contrib-2023.10-py2.py3-none-any.whl + pyinstaller_hooks_contrib-2024.1-py2.py3-none-any.whl pywin32_ctypes-0.2.2-py3-none-any.whl setuptools-68.2.2-py3-none-any.whl - upx-4.1.0-win32.zip ) [ $w10 ] && fns+=( - pyinstaller-6.1.0-py3-none-win_amd64.whl - Jinja2-3.1.2-py3-none-any.whl - MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl + pyinstaller-6.4.0-py3-none-win_amd64.whl + Jinja2-3.1.3-py3-none-any.whl + MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl mutagen-1.47.0-py3-none-any.whl packaging-23.2-py3-none-any.whl pillow-10.2.0-cp311-cp311-win_amd64.whl python-3.11.8-amd64.exe + upx-4.2.2-win64.zip ) [ $w7 ] && fns+=( pyinstaller-5.13.2-py3-none-win32.whl @@ -38,6 +38,7 @@ fns=( idna-3.4-py3-none-any.whl requests-2.28.2-py3-none-any.whl urllib3-1.26.14-py2.py3-none-any.whl + upx-4.2.2-win32.zip ) [ $w7 ] && fns+=( future-0.18.2.tar.gz From 503face97404cdfc4ac40a4ae702fc62e5b9a379 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 21:58:46 +0000 Subject: [PATCH 36/36] update pkgs to 1.10.2 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index b60c4034..fd2ecc3a 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: icxes pkgname=copyparty -pkgver="1.10.1" +pkgver="1.10.2" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, 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=("3969bbacccaa2fbb4c0bb1c971d9fd7d1851c35f829a1f2f02ad281f5f6dfe53") +sha256sums=("001be979a0fdd8ace7d48cab79a137c13b87b78be35fc9633430f45a2831c3ed") build() { cd "${srcdir}/${pkgname}-${pkgver}" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index c7955ea4..09ca4614 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.10.1/copyparty-sfx.py", - "version": "1.10.1", - "hash": "sha256-p1SF0BKY+qcs+/ZpqgU3dfK4E+/rpxezsiY6U1obhx4=" + "url": "https://github.com/9001/copyparty/releases/download/v1.10.2/copyparty-sfx.py", + "version": "1.10.2", + "hash": "sha256-O9lkN30gy3kwIH+39O4dN7agZPkuH36BDTk8mEsQCVg=" } \ No newline at end of file