From d636316a19ecf6ba269b0b5d7e4358ccba7cdf8e Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 10 Feb 2024 18:37:21 +0000 Subject: [PATCH] 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"]},