diff --git a/copyparty/__main__.py b/copyparty/__main__.py index cff92ab0..456cf0ed 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -693,6 +693,7 @@ def run_argparse( ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example \033[32m3921") ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example \033[32m3990") ap2.add_argument("--ftpv", action="store_true", help="verbose") + 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 SEC 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 fdd1f514..fbc73c7c 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -62,8 +62,7 @@ class FtpAuth(DummyAuthorizer): if username == "anonymous": uname = "*" else: - creds = password or username - uname = asrv.iacct.get(creds, "") if creds else "*" + uname = asrv.iacct.get(password, "") or asrv.iacct.get(username, "") or "*" if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): g = self.hub.gpwd @@ -164,8 +163,15 @@ class FtpFs(AbstractedFS): w = "w" in mode or "a" in mode or "+" in mode ap = self.rv2a(filename, r, w) - if w and bos.path.exists(ap): - raise FilesystemError("cannot open existing file for writing") + if w: + try: + st = bos.stat(ap) + td = time.time() - st.st_mtime + except: + td = 0 + + if td < -1 or td > self.args.ftp_wt: + raise FilesystemError("cannot open existing file for writing") self.validpath(ap) return open(fsenc(ap), mode) @@ -273,8 +279,11 @@ class FtpFs(AbstractedFS): return bos.lstat(ap) def isfile(self, path: str) -> bool: - st = self.stat(path) - return stat.S_ISREG(st.st_mode) + try: + st = self.stat(path) + return stat.S_ISREG(st.st_mode) + except: + return False # expected for mojibake in ftp_SIZE() def islink(self, path: str) -> bool: ap = self.rv2a(path) @@ -326,6 +335,9 @@ class FtpHandler(FTPHandler): # abspath->vpath mapping to resolve log_transfer paths self.vfs_map: dict[str, str] = {} + # reduce non-debug logging + self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")] + def ftp_STOR(self, file: str, mode: str = "w") -> Any: # Optional[str] vp = join(self.fs.cwd, file).lstrip("/")