From 9672b8c9b37e5b43b052ec29419137df314292c0 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 8 Dec 2023 01:11:03 +0000 Subject: [PATCH] ensure nested symlinks are not broken during deletes; when moving/deleting a file, all symlinked dupes are verified to ensure this action does not break any symlinks, however it did this by checking the realpath of each link. This was not good enough, since the deleted file may be a part of a series of nested symlinks this situation occurs because the deduper tries to keep relative symlinks as close as possible, only traversing into parent/sibling folders as required, which can lead to several levels of nested links --- copyparty/bos/bos.py | 4 ++++ copyparty/up2k.py | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/copyparty/bos/bos.py b/copyparty/bos/bos.py index a1981d31..c1128008 100644 --- a/copyparty/bos/bos.py +++ b/copyparty/bos/bos.py @@ -43,6 +43,10 @@ def open(p: str, *a, **ka) -> int: return os.open(fsenc(p), *a, **ka) +def readlink(p: str) -> str: + return fsdec(os.readlink(fsenc(p))) + + def rename(src: str, dst: str) -> None: return os.rename(fsenc(src), fsenc(dst)) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 0ddba707..fb64e5a9 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3617,6 +3617,8 @@ class Up2k(object): except: self.log("relink: not found: [{}]".format(ap)) + # self.log("full:\n" + "\n".join(" {:90}: {}".format(*x) for x in full.items())) + # self.log("links:\n" + "\n".join(" {:90}: {}".format(*x) for x in links.items())) if not dabs and not full and links: # deleting final remaining full copy; swap it with a symlink slabs = list(sorted(links.keys()))[0] @@ -3634,12 +3636,45 @@ class Up2k(object): dabs = list(sorted(full.keys()))[0] for alink, parts in links.items(): - lmod = None + lmod = 0.0 try: - if alink != sabs and absreal(alink) != sabs: - continue + faulty = False + ldst = alink + try: + for n in range(40): # MAXSYMLINKS + zs = bos.readlink(ldst) + ldst = os.path.join(os.path.dirname(ldst), zs) + ldst = bos.path.abspath(ldst) + if not bos.path.islink(ldst): + break - self.log("relinking [{}] to [{}]".format(alink, dabs)) + if ldst == sabs: + t = "relink because level %d would break:" + self.log(t % (n,), 6) + faulty = True + except Exception as ex: + self.log("relink because walk failed: %s; %r" % (ex, ex), 3) + faulty = True + + zs = absreal(alink) + if ldst != zs: + t = "relink because computed != actual destination:\n %s\n %s" + self.log(t % (ldst, zs), 3) + ldst = zs + faulty = True + + if bos.path.islink(ldst): + raise Exception("broken symlink: %s" % (alink,)) + + if alink != sabs and ldst != sabs and not faulty: + continue # original symlink OK; leave it be + + except Exception as ex: + t = "relink because symlink verification failed: %s; %r" + self.log(t % (ex, ex), 3) + + self.log("relinking [%s] to [%s]" % (alink, dabs)) + try: lmod = bos.path.getmtime(alink, False) bos.unlink(alink) except: