Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 82 additions & 29 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -69,37 +69,43 @@ 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()

else:
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)

Expand All @@ -123,15 +129,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)
Expand All @@ -148,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):
Expand Down Expand Up @@ -211,8 +236,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)

Expand Down Expand Up @@ -265,9 +290,36 @@ 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)

self.logger.debug(f"Junction: real path: {real_path}")
self.logger.debug(f"Junction: src real path: {os.path.realpath(src)}")

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))

def _handle_symlink(self, entry: os.DirEntry[str], src, dst):
Expand Down Expand Up @@ -425,8 +477,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}")

Expand All @@ -438,8 +490,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
Expand All @@ -465,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 :]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.