diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d9dc64dc..3c7e46e3 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1034,6 +1034,7 @@ def add_ftp(ap): ap2.add_argument("--ftpv", action="store_true", help="verbose") ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") + ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite") ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections") ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000") diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index a918b234..ea6a9798 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -19,6 +19,7 @@ from .__init__ import PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos from .util import ( + VF_CAREFUL, Daemon, ODict, Pebkac, @@ -30,6 +31,7 @@ from .util import ( runhook, sanitize_fn, vjoin, + wunlink, ) if TYPE_CHECKING: @@ -37,7 +39,7 @@ if TYPE_CHECKING: if True: # pylint: disable=using-constant-test import typing - from typing import Any, Optional + from typing import Any, Optional, Union class FSE(FilesystemError): @@ -139,6 +141,9 @@ class FtpFs(AbstractedFS): self.listdirinfo = self.listdir self.chdir(".") + def log(self, msg: str, c: Union[int, str] = 0) -> None: + self.hub.log("ftpd", msg, c) + def v2a( self, vpath: str, @@ -207,17 +212,37 @@ class FtpFs(AbstractedFS): w = "w" in mode or "a" in mode or "+" in mode ap = self.rv2a(filename, r, w)[0] + self.validpath(ap) if w: try: st = bos.stat(ap) td = time.time() - st.st_mtime + need_unlink = True except: + need_unlink = False td = 0 - if td < -1 or td > self.args.ftp_wt: - raise FSE("Cannot open existing file for writing") + if w and need_unlink: + if td >= -1 and td <= self.args.ftp_wt: + # within permitted timeframe; unlink and accept + do_it = True + elif self.args.no_del or self.args.ftp_no_ow: + # file too old, or overwrite not allowed; reject + do_it = False + else: + # allow overwrite if user has delete permission + # (avoids win2000 freaking out and deleting the server copy without uploading its own) + try: + self.rv2a(filename, False, True, False, True) + do_it = True + except: + do_it = False + + if not do_it: + raise FSE("File already exists") + + wunlink(self.log, ap, VF_CAREFUL) - self.validpath(ap) return open(fsenc(ap), mode, self.args.iobuf) def chdir(self, path: str) -> None: