From 1271459d15c181ad46553c414fc7d742d9dd6f2e Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:12:02 +0100 Subject: [PATCH 1/9] Remove auto removing of junctions - like folders, files indie can be checked Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 79a0ed4..3e269df 100644 --- a/main.py +++ b/main.py @@ -211,8 +211,8 @@ def _sync_item(self, entry: os.DirEntry[str], src, dst): self.logger.debug(f"Item sync: Destination {os.path.join(dst, entry.name)}") if entry.is_junction(): - if os.path.exists(os.path.join(dst, entry.name)): - self._remove(os.path.join(dst, entry.name)) + # if os.path.lexists(os.path.join(dst, entry.name)): + # self._remove(os.path.join(dst, entry.name)) self._handle_junction(entry, src, dst) From 3fb373738d3a2875bc7fc2eda70c9f9499f11a34 Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:28:37 +0100 Subject: [PATCH 2/9] Add junction recursion check Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.py b/main.py index 3e269df..f2c4bfe 100644 --- a/main.py +++ b/main.py @@ -268,6 +268,14 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): f"Junction in path {os.path.abspath(os.path.join(src, entry.name))}, recursing as regular folder..." ) + real_path = os.path.realpath(entry.path) + + self.logger.debug(f"Junction: real path: {real_path}") + + if os.path.realpath(src) in real_path: + self.logger.warning(f"Junction {entry.path} is recursive! Skipping") + return + self._sync_folder(os.path.join(src, entry.name), os.path.join(dst, entry.name)) def _handle_symlink(self, entry: os.DirEntry[str], src, dst): From 7f65385e951ac3d2cb874452fdf0221a8b64c517 Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:51:11 +0100 Subject: [PATCH 3/9] Change absolute source and replica paths to real paths Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index f2c4bfe..c6f9dbc 100644 --- a/main.py +++ b/main.py @@ -38,18 +38,18 @@ def __init__( self.dangle: bool = not dont_dangle self.odd: bool = odd - self.source_abs = os.path.abspath(self.source) - self.replica_abs = os.path.abspath(self.replica) + self.source_real = os.path.realpath(self.source) + self.replica_real = os.path.realpath(self.replica) self.encountered_inodes = {} self.logger = logging.getLogger(__name__) self.logger.info(f"Source: {self.source}") - self.logger.debug(f"Source absolute: {self.source_abs}") + self.logger.debug(f"Source real: {self.source_real}") self.logger.info(f"Replica: {self.replica}") - self.logger.debug(f"Replica absolute: {self.replica_abs}") + self.logger.debug(f"Replica real: {self.replica_real}") self.logger.debug(f"Interval: {self.interval} s") self.logger.debug(f"Count: {self.count}") @@ -69,30 +69,30 @@ def run(self): def _sync(self): """Synchronize source folder to replica folder.""" - if not os.path.exists(self.source_abs) or not os.path.isdir(self.source_abs): + if not os.path.exists(self.source_real) or not os.path.isdir(self.source_real): self.logger.error( - f"Source {self.source_abs} is not a directory or doesn't exist! Quitting" + f"Source {self.source_real} is not a directory or doesn't exist! Quitting" ) quit() - if os.path.commonpath([self.source_abs, self.replica_abs]) in [ - self.source_abs, - self.replica_abs, + if os.path.commonpath([self.source_real, self.replica_real]) in [ + self.source_real, + self.replica_real, ]: self.logger.error( - f"Replica {self.replica_abs} or source {self.source_abs} is relative to the other! Quitting" + f"Replica {self.replica_real} or source {self.source_real} is relative to the other! Quitting" ) quit() self.logger.info("Syncing...") try: - self._sync_folder(self.source, self.replica) + self._sync_folder(self.source_real, self.replica_real) except NotADirectoryError as e: - if not os.path.isdir(self.replica_abs): + if not os.path.isdir(self.replica_real): self.logger.error( - f"Replica {self.replica_abs} exists, but is not a directory! Quitting" + f"Replica {self.replica_real} exists, but is not a directory! Quitting" ) quit() @@ -433,8 +433,8 @@ def _get_symlink_target_path( if self.dangle: self.logger.debug("Symlink: --dont-dangle-symlinks is disabled") - if self.source_abs == os.path.commonpath( - [self.source_abs, symlink_target_path_absolute] + if self.source_real == os.path.commonpath( + [self.source_real, symlink_target_path_absolute] ): self.logger.debug(f"Symlink inside source, using {symlink_target_path}") @@ -446,8 +446,8 @@ def _get_symlink_target_path( return symlink_target_path elif not dangling: - if self.source_abs == os.path.commonpath( - [self.source_abs, symlink_target_path_absolute] + if self.source_real == os.path.commonpath( + [self.source_real, symlink_target_path_absolute] ): self.logger.debug(f"Symlink inside source, using {symlink_target_path}") return symlink_target_path From ee49579d704b830b0aea97b7a758a7cb5710d4e4 Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:55:05 +0100 Subject: [PATCH 4/9] Add junction target validation Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.py b/main.py index c6f9dbc..1a68bd8 100644 --- a/main.py +++ b/main.py @@ -276,6 +276,12 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): self.logger.warning(f"Junction {entry.path} is recursive! Skipping") return + if not os.path.exists(real_path): + self.logger.warning( + f"Junction {entry.path} target {real_path} does not exist! Skipping" + ) + return + self._sync_folder(os.path.join(src, entry.name), os.path.join(dst, entry.name)) def _handle_symlink(self, entry: os.DirEntry[str], src, dst): From 1751d661b062234bc16034257f34e3010e5baeeb Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:57:46 +0100 Subject: [PATCH 5/9] fixup! Add junction target validation Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 1a68bd8..87c36bb 100644 --- a/main.py +++ b/main.py @@ -271,6 +271,7 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): real_path = os.path.realpath(entry.path) self.logger.debug(f"Junction: real path: {real_path}") + self.logger.debug(f"Junction: src real path: {os.path.realpath(src)}") if os.path.realpath(src) in real_path: self.logger.warning(f"Junction {entry.path} is recursive! Skipping") From f7dd726b20a0efba0de18e4f7b25f2e37a13d296 Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:00:46 +0100 Subject: [PATCH 6/9] Fix junction recurse checking Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 87c36bb..ef32f77 100644 --- a/main.py +++ b/main.py @@ -273,7 +273,7 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): self.logger.debug(f"Junction: real path: {real_path}") self.logger.debug(f"Junction: src real path: {os.path.realpath(src)}") - if os.path.realpath(src) in real_path: + if real_path in os.path.realpath(src): self.logger.warning(f"Junction {entry.path} is recursive! Skipping") return From e1ab27755a03c0775c3e2415ef8e137452b01b03 Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:31:32 +0100 Subject: [PATCH 7/9] Drop inode check for hardlink recursion Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index ef32f77..c96ec11 100644 --- a/main.py +++ b/main.py @@ -123,15 +123,24 @@ def _sync_folder(self, src, dst): for i in src_entries: self.logger.debug(f"Syncing entry: {i}: Inode {i.inode()}") - if i.inode() in self.encountered_inodes.keys(): - self.logger.warning( - f"{i.path} was already encountered in {self.encountered_inodes[i.inode()]}, skipping!" - ) - - continue - - else: - self.encountered_inodes[i.inode()] = i.path + # Implementation of inode checking for hardlink recursion + # + # However, I was unable to find a way to make this work + # with junction handling + # + # So the decision was to work with the assumption + # of a tree-ish like structure and ignore + # hardlink recursion for the time being + # + # if i.inode() in self.encountered_inodes.keys(): + # self.logger.warning( + # f"{i.path} was already encountered in {self.encountered_inodes[i.inode()]}, skipping!" + # ) + # + # continue + # + # else: + # self.encountered_inodes[i.inode()] = i.path if i.name in dst_contents: same = self._compare_entry(i, src, dst) @@ -265,7 +274,7 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): self.logger.debug("Copy Junction") self.logger.warning( - f"Junction in path {os.path.abspath(os.path.join(src, entry.name))}, recursing as regular folder..." + f"Junction in path {os.path.abspath(os.path.join(src, entry.name))}, attempting recurse as a regular folder..." ) real_path = os.path.realpath(entry.path) From 87d71e269e8548a62fa569ebb13628b3c50dc3de Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:40:53 +0100 Subject: [PATCH 8/9] Add junction removing if they became invalid Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/main.py b/main.py index c96ec11..5946bd3 100644 --- a/main.py +++ b/main.py @@ -284,12 +284,24 @@ def _handle_junction(self, entry: os.DirEntry[str], src, dst): if real_path in os.path.realpath(src): self.logger.warning(f"Junction {entry.path} is recursive! Skipping") + + if os.path.lexists(os.path.join(dst, entry.name)): + # junction could be invalid, but present in dst -> remove from destination + # (e.g. junction became invalid between syncs) + self._remove(os.path.join(dst, entry.name)) + return if not os.path.exists(real_path): self.logger.warning( f"Junction {entry.path} target {real_path} does not exist! Skipping" ) + + if os.path.lexists(os.path.join(dst, entry.name)): + # junction could be invalid, but present in dst -> remove from destination + # (e.g. junction became invalid between syncs) + self._remove(os.path.join(dst, entry.name)) + return self._sync_folder(os.path.join(src, entry.name), os.path.join(dst, entry.name)) From f0605dff0ea46a0fc4ed1e3e553a6b8ea0bec1ac Mon Sep 17 00:00:00 2001 From: Martian <81148675+Martian-0007@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:53:31 +0100 Subject: [PATCH 9/9] Improve documentation Signed-off-by: Martian <81148675+Martian-0007@users.noreply.github.com> --- main.py | 17 +++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 5946bd3..57def47 100644 --- a/main.py +++ b/main.py @@ -100,6 +100,12 @@ def _sync(self): raise e def _sync_folder(self, src, dst): + """Sync source and destination folders. + + Args: + src: source folder path + dst: destination folder path + """ if not os.path.lexists(dst): self._mkdir(dst) @@ -157,6 +163,16 @@ def _sync_folder(self, src, dst): self._sync_item(i, src, dst) def _compare_entry(self, entry: os.DirEntry[str], src, dst) -> bool: + """Check whether an entry was already synced or not. + + Args: + entry: os.DirEntry of the source object + src: current working source path + dst: current working destination path + + Returns: + bool: True if entry was already synced, else False + """ same = False # By default, assume source and destination are different if entry.is_dir(follow_symlinks=False): @@ -501,6 +517,7 @@ def _detranslate_symlink_target_path( Raises: ValueError: if the symlink target is not a previously translated symlink target """ + # Example implementation of a detranslation function for synced translated symlinks self.logger.debug(f"Symlink target path on disk: {symlink_target_path}") detranslated = symlink_target_path[symlink_target_path.index("/./") + 3 :] diff --git a/pyproject.toml b/pyproject.toml index 45c00de..f942243 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "folder-syncer" -version = "1.1.1.dev1" +version = "1.1.1.dev2" description = "Folder syncer for Veeam technical assessment" readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index b3d4a5b..de4179e 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.13" [[package]] name = "folder-syncer" -version = "1.1.1.dev1" +version = "1.1.1.dev2" source = { virtual = "." }