mirror of
				https://github.com/9001/copyparty.git
				synced 2025-11-04 05:43:17 +00:00 
			
		
		
		
	add tftp server
This commit is contained in:
		
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								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
 | 
			
		||||
 
 | 
			
		||||
@@ -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')
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										241
									
								
								copyparty/tftpd.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								copyparty/tftpd.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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   |   |   |   | █ | █ |   | █ | █ | █ |   | █ | █ |
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ thumbnails2 = ["pyvips"]
 | 
			
		||||
audiotags = ["mutagen"]
 | 
			
		||||
ftpd = ["pyftpdlib"]
 | 
			
		||||
ftps = ["pyftpdlib", "pyopenssl"]
 | 
			
		||||
tftpd = ["partftpy"]
 | 
			
		||||
pwhash = ["argon2-cffi"]
 | 
			
		||||
 | 
			
		||||
[project.scripts]
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								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"]},
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user