add tftp server

This commit is contained in:
ed 2024-02-10 18:37:21 +00:00
parent ed524d84bb
commit d636316a19
14 changed files with 348 additions and 26 deletions

View file

@ -3,7 +3,7 @@
turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser 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 * 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) * 📱 [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 👉 **[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` * [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921`
* [webdav server](#webdav-server) - with read-write support * [webdav server](#webdav-server) - with read-write support
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI * [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 * [smb server](#smb-server) - unsafe, slow, not recommended for wan
* [browser ux](#browser-ux) - tweaking the ui * [browser ux](#browser-ux) - tweaking the ui
* [file indexing](#file-indexing) - enables dedup and music search ++ * [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: 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={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=12000-12099/tcp # --zone=libvirt
firewall-cmd --permanent --add-port={1900,5353}/udp # --zone=libvirt firewall-cmd --permanent --add-port={69,1900,3969,5353}/udp # --zone=libvirt
firewall-cmd --reload 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 ## features
@ -172,6 +173,7 @@ firewall-cmd --reload
* ☑ volumes (mountpoints) * ☑ volumes (mountpoints)
* ☑ [accounts](#accounts-and-volumes) * ☑ [accounts](#accounts-and-volumes)
* ☑ [ftp server](#ftp-server) * ☑ [ftp server](#ftp-server)
* ☑ [tftp server](#tftp-server)
* ☑ [webdav server](#webdav-server) * ☑ [webdav server](#webdav-server)
* ☑ [smb/cifs server](#smb-server) * ☑ [smb/cifs server](#smb-server)
* ☑ [qr-code](#qr-code) for quick access * ☑ [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) * 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 ## smb server
unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write

View file

@ -2,7 +2,7 @@
pkgname=copyparty pkgname=copyparty
pkgver="1.9.31" pkgver="1.9.31"
pkgrel=1 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") arch=("any")
url="https://github.com/9001/${pkgname}" url="https://github.com/9001/${pkgname}"
license=('MIT') license=('MIT')

View file

@ -46,6 +46,7 @@ from .util import (
PY_DESC, PY_DESC,
PYFTPD_VER, PYFTPD_VER,
SQLITE_VER, SQLITE_VER,
PARTFTPY_VER,
UNPLICATIONS, UNPLICATIONS,
align_tab, align_tab,
ansi_re, ansi_re,
@ -993,7 +994,7 @@ def add_zc_ssdp(ap):
def add_ftp(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("--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("--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") 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)") 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): def add_smb(ap):
ap2 = ap.add_argument_group('SMB/CIFS options') 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!") 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_transcoding(ap)
add_ftp(ap) add_ftp(ap)
add_webdav(ap) add_webdav(ap)
add_tftp(ap)
add_smb(ap) add_smb(ap)
add_safety(ap) add_safety(ap)
add_salt(ap, fk_salt, ah_salt) add_salt(ap, fk_salt, ah_salt)
@ -1375,7 +1385,7 @@ def main(argv: Optional[list[str]] = None) -> None:
if argv is None: if argv is None:
argv = sys.argv 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( f = f.format(
S_VERSION, S_VERSION,
CODENAME, CODENAME,
@ -1384,6 +1394,7 @@ def main(argv: Optional[list[str]] = None) -> None:
SQLITE_VER, SQLITE_VER,
JINJA_VER, JINJA_VER,
PYFTPD_VER, PYFTPD_VER,
PARTFTPY_VER,
) )
lprint(f) lprint(f)

View file

@ -133,7 +133,7 @@ class SvcHub(object):
if not self._process_config(): if not self._process_config():
raise Exception(BAD_CFG) raise Exception(BAD_CFG)
# for non-http clients (ftp) # for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
@ -268,6 +268,12 @@ class SvcHub(object):
Daemon(self.start_ftpd, "start_ftpd") Daemon(self.start_ftpd, "start_ftpd")
zms += "f" if args.ftp else "F" 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: if args.smb:
# impacket.dcerpc is noisy about listen timeouts # impacket.dcerpc is noisy about listen timeouts
sto = socket.getdefaulttimeout() sto = socket.getdefaulttimeout()
@ -297,10 +303,12 @@ class SvcHub(object):
def start_ftpd(self) -> None: def start_ftpd(self) -> None:
time.sleep(30) 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: def restart_ftpd(self) -> None:
if not hasattr(self, "ftpd"): if not hasattr(self, "ftpd"):
@ -317,6 +325,17 @@ class SvcHub(object):
self.ftpd = Ftpd(self) self.ftpd = Ftpd(self)
self.log("root", "started FTPd") 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: def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5) time.sleep(1 if self.args.ign_ebind_all else 5)
expected = self.broker.num_workers * self.tcpsrv.nsrv expected = self.broker.num_workers * self.tcpsrv.nsrv
@ -444,6 +463,7 @@ class SvcHub(object):
al.xff_re = self._ipa2re(al.xff_src) al.xff_re = self._ipa2re(al.xff_src)
al.ipa_re = self._ipa2re(al.ipa) al.ipa_re = self._ipa2re(al.ipa)
al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or 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) mte = ODict.fromkeys(DEF_MTE.split(","), True)
al.mte = odfusion(mte, al.mte) al.mte = odfusion(mte, al.mte)

View file

@ -309,6 +309,7 @@ class TcpSrv(object):
self.hub.start_zeroconf() self.hub.start_zeroconf()
gencert(self.log, self.args, self.netdevs) gencert(self.log, self.args, self.netdevs)
self.hub.restart_ftpd() self.hub.restart_ftpd()
self.hub.restart_tftpd()
def shutdown(self) -> None: def shutdown(self) -> None:
self.stopping = True self.stopping = True

241
copyparty/tftpd.py Normal file
View file

@ -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)

View file

@ -423,16 +423,21 @@ try:
except: except:
PYFTPD_VER = "(None)" PYFTPD_VER = "(None)"
try:
from partftpy.__init__ import __version__ as PARTFTPY_VER
except:
PARTFTPY_VER = "(None)"
PY_DESC = py_desc() PY_DESC = py_desc()
VERSIONS = "copyparty v{} ({})\n{}\n sqlite v{} | jinja v{} | pyftpd v{}".format( VERSIONS = "copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {}".format(
S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER 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) _: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
__all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER"] __all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER", "PARTFTPY_VER"]
class Daemon(threading.Thread): class Daemon(threading.Thread):
@ -536,6 +541,8 @@ class HLog(logging.Handler):
elif record.name.startswith("impacket"): elif record.name.startswith("impacket"):
if self.ptn_smb_ign.match(msg): if self.ptn_smb_ign.match(msg):
return return
elif record.name.startswith("partftpy."):
record.name = record.name[9:]
self.log_func(record.name[-21:], msg, c) self.log_func(record.name[-21:], msg, c)

View file

@ -242,6 +242,7 @@ python3 -m venv .venv
pip install jinja2 strip_hints # MANDATORY pip install jinja2 strip_hints # MANDATORY
pip install mutagen # audio metadata pip install mutagen # audio metadata
pip install pyftpdlib # ftp server 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 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 Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install pyvips # faster thumbnails pip install pyvips # faster thumbnails

View file

@ -24,6 +24,10 @@ https://github.com/giampaolo/pyftpdlib/
C: 2007 Giampaolo Rodola C: 2007 Giampaolo Rodola
L: MIT L: MIT
https://github.com/9001/partftpy
C: 2010-2021 Michael P. Soulier
L: MIT
https://github.com/nayuki/QR-Code-generator/ https://github.com/nayuki/QR-Code-generator/
C: Project Nayuki C: Project Nayuki
L: MIT L: MIT

View file

@ -200,9 +200,10 @@ symbol legend,
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | | serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ |
| serve ftp | █ | | | | | █ | | | | | | █ | | serve ftp (tcp) | █ | | | | | █ | | | | | | █ |
| serve ftps | █ | | | | | █ | | | | | | █ | | serve ftps (tls) | █ | | | | | █ | | | | | | █ |
| serve sftp | | | | | | █ | | | | | | █ | | serve tftp (udp) | █ | | | | | | | | | | | |
| serve sftp (ssh) | | | | | | █ | | | | | | █ |
| serve smb/cifs | | | | | | █ | | | | | | | | serve smb/cifs | | | | | | █ | | | | | | |
| serve dlna | | | | | | █ | | | | | | | | serve dlna | | | | | | █ | | | | | | |
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ | █ | | listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ | █ |

View file

@ -48,6 +48,7 @@ thumbnails2 = ["pyvips"]
audiotags = ["mutagen"] audiotags = ["mutagen"]
ftpd = ["pyftpdlib"] ftpd = ["pyftpdlib"]
ftps = ["pyftpdlib", "pyopenssl"] ftps = ["pyftpdlib", "pyopenssl"]
tftpd = ["partftpy"]
pwhash = ["argon2-cffi"] pwhash = ["argon2-cffi"]
[project.scripts] [project.scripts]

View file

@ -26,8 +26,9 @@ help() { exec cat <<'EOF'
# _____________________________________________________________________ # _____________________________________________________________________
# core features: # core features:
# #
# `no-ftp` saves ~33k by removing the ftp server and filetype detector, # `no-ftp` saves ~30k by removing the ftp server, disabling --ftp
# disabling --ftpd and --magic #
# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp
# #
# `no-smb` saves ~3.5k by removing the smb / cifs server # `no-smb` saves ~3.5k by removing the smb / cifs server
# #
@ -114,6 +115,7 @@ while [ ! -z "$1" ]; do
gz) use_gz=1 ; ;; gz) use_gz=1 ; ;;
gzz) shift;use_gzz=$1;use_gz=1; ;; gzz) shift;use_gzz=$1;use_gz=1; ;;
no-ftp) no_ftp=1 ; ;; no-ftp) no_ftp=1 ; ;;
no-tfp) no_tfp=1 ; ;;
no-smb) no_smb=1 ; ;; no-smb) no_smb=1 ; ;;
no-zm) no_zm=1 ; ;; no-zm) no_zm=1 ; ;;
no-fnt) no_fnt=1 ; ;; no-fnt) no_fnt=1 ; ;;
@ -165,7 +167,8 @@ necho() {
[ $repack ] && { [ $repack ] && {
old="$tmpdir/pe-copyparty.$(id -u)" old="$tmpdir/pe-copyparty.$(id -u)"
echo "repack of files in $old" 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 cp -pR "$old/"*ftp . || true
} }
@ -221,6 +224,16 @@ necho() {
mkdir ftp/ mkdir ftp/
mv pyftpdlib 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 necho collecting python-magic
v=0.4.27 v=0.4.27
f="../build/python-magic-$v.tar.gz" f="../build/python-magic-$v.tar.gz"
@ -234,7 +247,6 @@ necho() {
rm -rf python-magic-* rm -rf python-magic-*
rm magic/compat.py rm magic/compat.py
iawk '/^def _add_compat/{o=1} !o; /^_add_compat/{o=0}' magic/__init__.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, # enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance # 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 rm -f ftp/pyftpdlib/{__main__,prefork}.py
[ $no_ftp ] && [ $no_ftp ] &&
rm -rf copyparty/ftpd.py ftp && rm -rf copyparty/ftpd.py ftp
sed -ri '/\.ftp/d' copyparty/svchub.py
[ $no_tfp ] &&
rm -rf copyparty/tftpd.py partftpy
[ $no_smb ] && [ $no_smb ] &&
rm -f copyparty/smbd.py rm -f copyparty/smbd.py
@ -584,7 +598,7 @@ nf=$(ls -1 "$zdir"/arc.* 2>/dev/null | wc -l)
echo gen tarlist 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/' | LC_ALL=C sort |
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1 sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1

View file

@ -54,6 +54,7 @@ copyparty/sutil.py,
copyparty/svchub.py, copyparty/svchub.py,
copyparty/szip.py, copyparty/szip.py,
copyparty/tcpsrv.py, copyparty/tcpsrv.py,
copyparty/tftpd.py,
copyparty/th_cli.py, copyparty/th_cli.py,
copyparty/th_srv.py, copyparty/th_srv.py,
copyparty/u2idx.py, copyparty/u2idx.py,

View file

@ -84,7 +84,7 @@ args = {
"version": about["__version__"], "version": about["__version__"],
"description": ( "description": (
"Portable file server with accelerated resumable uploads, " "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" + "video thumbnails, audio transcoding, and write-only folders"
), ),
"long_description": long_description, "long_description": long_description,
@ -140,6 +140,7 @@ args = {
"audiotags": ["mutagen"], "audiotags": ["mutagen"],
"ftpd": ["pyftpdlib"], "ftpd": ["pyftpdlib"],
"ftps": ["pyftpdlib", "pyopenssl"], "ftps": ["pyftpdlib", "pyopenssl"],
"tftpd": ["partftpy"],
"pwhash": ["argon2-cffi"], "pwhash": ["argon2-cffi"],
}, },
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},