From 83178d08369708d6e9ba48b5a9b458fc19723715 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 29 Apr 2023 11:30:43 +0000 Subject: [PATCH] preserve empty folders (closes #23): * when deleting files, do not cascade upwards through empty folders * when moving folders, also move any empty folders inside the only remaining action which autoremoves empty folders is files getting deleted as they expire volume lifetimes also prevents accidentally moving parent folders into subfolders (even though that actually worked surprisingly well) --- copyparty/ftpd.py | 2 +- copyparty/httpcli.py | 19 ++++++------------- copyparty/smbd.py | 2 +- copyparty/up2k.py | 39 ++++++++++++++++++++++++++++++--------- copyparty/util.py | 2 +- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index e67b822d..c957782e 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -271,7 +271,7 @@ class FtpFs(AbstractedFS): vp = join(self.cwd, path).lstrip("/") try: - self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], []) + self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False) except Exception as ex: raise FSE(str(ex)) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index fb226913..a8a316aa 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1152,18 +1152,9 @@ class HttpCli(object): dst = self.headers["destination"] dst = re.sub("^https?://[^/]+", "", dst).lstrip() dst = unquotep(dst) - if not self._mv(self.vpath, dst): + if not self._mv(self.vpath, dst.lstrip("/")): return False - # up2k only cares about files and removes all empty folders; - # clients naturally expect empty folders to survive a rename - vn, rem = self.asrv.vfs.get(dst, self.uname, False, False) - dabs = vn.canonical(rem) - try: - bos.makedirs(dabs) - except: - pass - return True def _applesan(self) -> bool: @@ -3054,7 +3045,7 @@ class HttpCli(object): ret = self.gen_tree(top, dst) if self.is_vproxied: parents = self.args.R.split("/") - for parent in parents[::-1]: + for parent in reversed(parents): ret = {"k%s" % (parent,): ret, "a": []} zs = json.dumps(ret) @@ -3193,7 +3184,9 @@ class HttpCli(object): nlim = int(self.uparam.get("lim") or 0) lim = [nlim, nlim] if nlim else [] - x = self.conn.hsrv.broker.ask("up2k.handle_rm", self.uname, self.ip, req, lim) + x = self.conn.hsrv.broker.ask( + "up2k.handle_rm", self.uname, self.ip, req, lim, False + ) self.loud_reply(x.get()) return True @@ -3210,7 +3203,7 @@ class HttpCli(object): # x-www-form-urlencoded (url query part) uses # either + or %20 for 0x20 so handle both dst = unquotep(dst.replace("+", " ")) - return self._mv(self.vpath, dst) + return self._mv(self.vpath, dst.lstrip("/")) def _mv(self, vsrc: str, vdst: str) -> bool: if not self.can_move: diff --git a/copyparty/smbd.py b/copyparty/smbd.py index ac5cdab4..d0ee688d 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -261,7 +261,7 @@ class SMB(object): yeet("blocked delete (no-del-acc): " + vpath) vpath = vpath.replace("\\", "/").lstrip("/") - self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], []) + self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [], False) def _utime(self, vpath: str, times: tuple[float, float]) -> None: if not self.args.smbw: diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 0e6c74a3..a291c647 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -384,7 +384,7 @@ class Up2k(object): if vp: fvp = "%s/%s" % (vp, fvp) - self._handle_rm(LEELOO_DALLAS, "", fvp, []) + self._handle_rm(LEELOO_DALLAS, "", fvp, [], True) nrm += 1 if nrm: @@ -2897,7 +2897,9 @@ class Up2k(object): except: pass - def handle_rm(self, uname: str, ip: str, vpaths: list[str], lim: list[int]) -> str: + def handle_rm( + self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool + ) -> str: n_files = 0 ok = {} ng = {} @@ -2906,7 +2908,7 @@ class Up2k(object): self.log("hit delete limit of {} files".format(lim[1]), 3) break - a, b, c = self._handle_rm(uname, ip, vp, lim) + a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up) n_files += a for k in b: ok[k] = 1 @@ -2920,7 +2922,7 @@ class Up2k(object): return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing) def _handle_rm( - self, uname: str, ip: str, vpath: str, lim: list[int] + self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool ) -> tuple[int, list[str], list[str]]: self.db_act = time.time() try: @@ -3027,16 +3029,22 @@ class Up2k(object): if xad: runhook(self.log, xad, abspath, vpath, "", uname, 0, 0, ip, 0, "") - ok: list[str] = [] - ng: list[str] = [] if is_dir: ok, ng = rmdirs(self.log_func, scandir, True, atop, 1) + else: + ok = ng = [] - ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop) + if rm_up: + ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop) + else: + ok2 = ng2 = [] return n_files, ok + ok2, ng + ng2 def handle_mv(self, uname: str, svp: str, dvp: str) -> str: + if svp == dvp or dvp.startswith(svp + "/"): + raise Pebkac(400, "mv: cannot move parent into subfolder") + svn, srem = self.asrv.vfs.get(svp, uname, True, False, True) svn, srem = svn.get_dbv(srem) sabs = svn.canonical(srem, False) @@ -3090,8 +3098,21 @@ class Up2k(object): curs.clear() - rmdirs(self.log_func, scandir, True, sabs, 1) - rmdirs_up(os.path.dirname(sabs), svn.realpath) + rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1) + + for zsl in (rm_ok, rm_ng): + for ap in reversed(zsl): + if not ap.startswith(sabs): + raise Pebkac(500, "mv_d: bug at {}, top {}".format(ap, sabs)) + + rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/") + vp = vjoin(dvp, rem) + try: + dvn, drem = self.asrv.vfs.get(vp, uname, False, True) + bos.mkdir(dvn.canonical(drem)) + except: + pass + return "k" def _mv_file( diff --git a/copyparty/util.py b/copyparty/util.py index 94371441..5247a70e 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2270,7 +2270,7 @@ def rmdirs( dirs = [os.path.join(top, x) for x in dirs] ok = [] ng = [] - for d in dirs[::-1]: + for d in reversed(dirs): a, b = rmdirs(logger, scandir, lstat, d, depth + 1) ok += a ng += b