mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
add tftp server
This commit is contained in:
parent
ed524d84bb
commit
d636316a19
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,11 +303,13 @@ class SvcHub(object):
|
|||
|
||||
def start_ftpd(self) -> None:
|
||||
time.sleep(30)
|
||||
if self.ftpd:
|
||||
return
|
||||
|
||||
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"):
|
||||
return
|
||||
|
@ -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"]},
|
||||
|
|
Loading…
Reference in a new issue