#!/usr/bin/python3
import os
import shutil
import logging
from pathlib import Path
from typing import Iterable
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("sync_engine")
class HotConfigSyncer:
"""
Maps configuration from a source to a live destination.
"""
def __init__(self, src_root: str | Path, dst_root: str | Path, folds: Iterable[str], masks: Iterable[str]):
self.src_root = Path(src_root).resolve()
self.dst_root = Path(dst_root).resolve()
self.folds = list(folds)
self.masks = list(masks)
def _log(self, action: str, path: Path, details: str = ""):
suffix = f" ({details})" if details else ""
logger.info(f"[{action:^10}] {path}{suffix}")
def _is_match(self, path: Path, patterns: list[str]) -> bool:
"""Match relative paths vs globs (e.g. 'postgresql-*', 'sub[gu]id')."""
return any(path.match(p) for p in patterns)
def _check_symlink(self, name: str, parent_fd: int, expected_target: str) -> bool:
"""Atomic symlink target compare."""
try:
fd = os.open(name, os.O_PATH | os.O_NOFOLLOW, dir_fd=parent_fd)
try:
return os.readlink("", dir_fd = fd) == expected_target
finally:
os.close(fd)
except OSError:
return False
def _apply_fold(self, name: str, parent_fd: int, target: str, rel_path: Path):
"""Nuke and replace destination directory with a single symlink."""
if self._check_symlink(name, parent_fd, target):
self._log("UNCHANGED", rel_path, "already folded")
return
full_dst = self.dst_root / rel_path
if full_dst.exists() or full_dst.is_symlink():
if full_dst.is_dir() and not full_dst.is_symlink():
shutil.rmtree(full_dst)
else:
os.unlink(name, dir_fd=parent_fd)
self._log("FOLD", rel_path, f"-> {target}")
os.symlink(target, name, dir_fd=parent_fd)
def _apply_explode(self, name: str, parent_fd: int, rel_path: Path):
"""Ensure a real directory exists to traverse into."""
try:
os.mkdir(name, dir_fd=parent_fd)
self._log("EXPLODE", rel_path, "created directory")
except FileExistsError:
full_dst = self.dst_root / rel_path
if not full_dst.is_dir() or full_dst.is_symlink():
self._log("TRANSITION", rel_path, "file -> directory")
os.unlink(name, dir_fd=parent_fd)
os.mkdir(name, dir_fd=parent_fd)
def _apply_link(self, name: str, parent_fd: int, target: str, rel_path: Path):
"""Symlinks an individual file."""
if self._check_symlink(name, parent_fd, target):
return
full_dst = self.dst_root / rel_path
try:
os.unlink(name, dir_fd=parent_fd)
except FileNotFoundError:
pass
except IsADirectoryError:
shutil.rmtree(full_dst)
self._log("LINK", rel_path, f"-> {target}")
os.symlink(target, name, dir_fd=parent_fd)
def sync(self):
"""Execute config sync"""
# Anchor destination root for relative fd operations
dst_root_fd = os.open(self.dst_root, os.O_RDONLY | os.O_DIRECTORY)
dst_fds = { Path("."): dst_root_fd }
try:
for root_str, dirs, files, _ in os.fwalk(self.src_root):
root = Path(root_str)
rel_root = root.relative_to(self.src_root)
parent_dst_fd = dst_fds[rel_root]
# Process directories in reverse for safe popping
for i in range(len(dirs) - 1, -1, -1):
d_name = dirs[i]
rel_path = rel_root / d_name
if self._is_match(rel_path, self.masks):
self._log("MASKED", rel_path)
dirs.pop(i)
continue
target = os.path.relpath(root / d_name, self.dst_root / rel_root)
if self._is_match(rel_path, self.folds):
self._apply_fold(d_name, parent_dst_fd, target, rel_path)
dirs.pop(i)
else:
self._apply_explode(d_name, parent_dst_fd, rel_path)
# Open + persist the dirfd for its children
dst_fds[rel_path] = os.open(d_name, os.O_RDONLY | os.O_DIRECTORY, dir_fd=parent_dst_fd)
for f_name in files:
rel_path = rel_root / f_name
if self._is_match(rel_path, self.masks):
self._log("MASKED", rel_path)
continue
target = os.path.relpath(root / f_name, self.dst_root / rel_root)
self._apply_link(f_name, parent_dst_fd, target, rel_path)
finally:
for fd in dst_fds.values():
os.close(fd)
def main():
syncer = HotConfigSyncer(
src_root="/dotfiles/common/etc",
dst_root="/etc",
folds=[
"NetworkManager", "portage", "ssh", "syslog-ng", "local.d", "X11"
],
masks=[
"nsswitch.conf", "*shadow", "group", "passwd", "sub[gu]id",
"postgresql-*", "chromium", "bluetooth", "conky", "cups", "opt"
]
)
syncer.sync()
main()[ MASKED ] chromium
[ MASKED ] opt
[ MASKED ] cups
[ FOLD ] NetworkManager (-> ../dotfiles/common/etc/NetworkManager)
[ EXPLODE ] network (created directory)
[ EXPLODE ] mplayer (created directory)
[ EXPLODE ] openal (created directory)
[ MASKED ] bluetooth
[ MASKED ] postgresql-9.6
[ EXPLODE ] layman (created directory)
[ FOLD ] local.d (-> ../dotfiles/common/etc/local.d)
[ FOLD ] X11 (-> ../dotfiles/common/etc/X11)
[ EXPLODE ] polipo (created directory)
[ MASKED ] conky
[ FOLD ] syslog-ng (-> ../dotfiles/common/etc/syslog-ng)
[ FOLD ] ssh (-> ../dotfiles/common/etc/ssh)
[ FOLD ] portage (-> ../dotfiles/common/etc/portage)
[ EXPLODE ] samba (created directory)
[ LINK ] eix-sync.conf (-> ../dotfiles/common/etc/eix-sync.conf)
[ LINK ] fstab (-> ../dotfiles/common/etc/fstab)
[ LINK ] login.defs (-> ../dotfiles/common/etc/login.defs)
[ LINK ] dracut.conf (-> ../dotfiles/common/etc/dracut.conf)
[ LINK ] ntp.conf (-> ../dotfiles/common/etc/ntp.conf)
[ LINK ] timezone (-> ../dotfiles/common/etc/timezone)
[ LINK ] libao.conf (-> ../dotfiles/common/etc/libao.conf)
[ LINK ] rc.conf (-> ../dotfiles/common/etc/rc.conf)
[ MASKED ] subuid
[ MASKED ] subgid
[ LINK ] host.conf (-> ../dotfiles/common/etc/host.conf)
[ LINK ] localtime (-> ../dotfiles/common/etc/localtime)
[ MASKED ] nsswitch.conf
[ LINK ] sddm.conf (-> ../dotfiles/common/etc/sddm.conf)
[ LINK ] xorg.conf (-> ../dotfiles/common/etc/xorg.conf)
[ LINK ] exports (-> ../dotfiles/common/etc/exports)
[ LINK ] fuse.conf (-> ../dotfiles/common/etc/fuse.conf)
[ LINK ] xattr.conf (-> ../dotfiles/common/etc/xattr.conf)
[ LINK ] shells (-> ../dotfiles/common/etc/shells)
[ LINK ] sudoers (-> ../dotfiles/common/etc/sudoers)
[ LINK ] profile2 (-> ../dotfiles/common/etc/profile2)
[ LINK ] profile (-> ../dotfiles/common/etc/profile)
[ LINK ] sysctl.conf (-> ../dotfiles/common/etc/sysctl.conf)
[ MASKED ] passwd
[ MASKED ] group
[ MASKED ] gshadow
[ LINK ] locale.gen (-> ../dotfiles/common/etc/locale.gen)
[ LINK ] inittab (-> ../dotfiles/common/etc/inittab)
[ MASKED ] shadow
[ LINK ] gitconfig (-> ../dotfiles/common/etc/gitconfig)
[ LINK ] resolv.conf (-> ../dotfiles/common/etc/resolv.conf)
[ LINK ] bash/bashrc (-> ../../dotfiles/common/etc/bash/bashrc)
[ LINK ] bash/completion.blacklist (-> ../../dotfiles/common/etc/bash/completion.blacklist)
[ LINK ] bash/._cfg0000_bashrc (-> ../../dotfiles/common/etc/bash/._cfg0000_bashrc)
[ LINK ] bash/._cfg0001_bashrc (-> ../../dotfiles/common/etc/bash/._cfg0001_bashrc)
[ LINK ] bash/._cfg0000_bash_logout (-> ../../dotfiles/common/etc/bash/._cfg0000_bash_logout)
[ LINK ] bash/bashrc.d/._cfg0000_bash_completion.sh (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_bash_completion.sh)
[ LINK ] bash/bashrc.d/kitty.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/kitty.bash)
[ LINK ] bash/bashrc.d/.keep_app-shells_bash-0 (-> ../../../dotfiles/common/etc/bash/bashrc.d/.keep_app-shells_bash-0)
[ LINK ] bash/bashrc.d/._cfg0000_10-gentoo-title.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_10-gentoo-title.bash)
[ LINK ] bash/bashrc.d/._cfg0001_10-gentoo-title.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0001_10-gentoo-title.bash)
[ LINK ] bash/bashrc.d/._cfg0000_10-gentoo-color.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_10-gentoo-color.bash)
[ LINK ] samba/.smb.conf.swp (-> ../../dotfiles/common/etc/samba/.smb.conf.swp)
[ LINK ] samba/smb.conf (-> ../../dotfiles/common/etc/samba/smb.conf)
[ LINK ] conf.d/zram-init (-> ../../dotfiles/common/etc/conf.d/zram-init)
[ LINK ] conf.d/net (-> ../../dotfiles/common/etc/conf.d/net)
[ LINK ] conf.d/nfsmount (-> ../../dotfiles/common/etc/conf.d/nfsmount)
[ LINK ] conf.d/ntp-client (-> ../../dotfiles/common/etc/conf.d/ntp-client)
[ LINK ] conf.d/ntpd (-> ../../dotfiles/common/etc/conf.d/ntpd)
[ LINK ] conf.d/nfs (-> ../../dotfiles/common/etc/conf.d/nfs)
[ LINK ] conf.d/consolefont (-> ../../dotfiles/common/etc/conf.d/consolefont)
[ MASKED ] conf.d/postgresql-9.5
[ LINK ] conf.d/keymaps (-> ../../dotfiles/common/etc/conf.d/keymaps)
[ MASKED ] conf.d/postgresql-9.6
[ LINK ] conf.d/postgresql (-> ../../dotfiles/common/etc/conf.d/postgresql)
[ LINK ] polipo/config.direct (-> ../../dotfiles/common/etc/polipo/config.direct)
[ LINK ] polipo/config.tor (-> ../../dotfiles/common/etc/polipo/config.tor)
[ LINK ] grub.d/41_custom (-> ../../dotfiles/common/etc/grub.d/41_custom)
[ LINK ] grub.d/40_custom (-> ../../dotfiles/common/etc/grub.d/40_custom)
[ LINK ] grub.d/30_os-prober (-> ../../dotfiles/common/etc/grub.d/30_os-prober)
[ LINK ] grub.d/20_linux_xen (-> ../../dotfiles/common/etc/grub.d/20_linux_xen)
[ LINK ] grub.d/10_linux (-> ../../dotfiles/common/etc/grub.d/10_linux)
[ LINK ] grub.d/00_header (-> ../../dotfiles/common/etc/grub.d/00_header)
[ LINK ] grub.d/README (-> ../../dotfiles/common/etc/grub.d/README)
[ LINK ] env.d/999custom (-> ../../dotfiles/common/etc/env.d/999custom)
[ LINK ] env.d/02locale (-> ../../dotfiles/common/etc/env.d/02locale)
[ LINK ] env.d/50baselayout (-> ../../dotfiles/common/etc/env.d/50baselayout)
[ EXPLODE ] polkit-1/conf.d (created directory)
[ LINK ] polkit-1/rules.d/49-polkit-pkla-compat.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/49-polkit-pkla-compat.rules)
[ LINK ] polkit-1/rules.d/50-default.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/50-default.rules)
[ LINK ] polkit-1/rules.d/10-local.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/10-local.rules)
[ LINK ] polkit-1/conf.d/10-local.conf (-> ../../../dotfiles/common/etc/polkit-1/conf.d/10-local.conf)
[ LINK ] layman/layman.cfg (-> ../../dotfiles/common/etc/layman/layman.cfg)
[ LINK ] udev/rules.d/99-myrules.rules (-> ../../../dotfiles/common/etc/udev/rules.d/99-myrules.rules)
[ LINK ] openal/alsoft.conf (-> ../../dotfiles/common/etc/openal/alsoft.conf)
[ LINK ] mplayer/mplayer.conf (-> ../../dotfiles/common/etc/mplayer/mplayer.conf)
[ EXPLODE ] network/if-down.d (created directory)
[ EXPLODE ] network/if-pre-up.d (created directory)
[ EXPLODE ] network/if-post-down.d (created directory)
[ EXPLODE ] network/interfaces.d (created directory)
[ EXPLODE ] network/if-up.d (created directory)
[ LINK ] network/interfaces (-> ../../dotfiles/common/etc/network/interfaces)
[ LINK ] network/ifupdown-ng.conf (-> ../../dotfiles/common/etc/network/ifupdown-ng.conf)
[ LINK ] network/if-up.d/avahi-autoipd (-> ../../../dotfiles/common/etc/network/if-up.d/avahi-autoipd)
[ LINK ] network/if-up.d/ethtool (-> ../../../dotfiles/common/etc/network/if-up.d/ethtool)
[ LINK ] network/if-up.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-up.d/wpasupplicant)
[ LINK ] network/interfaces.d/setup (-> ../../../dotfiles/common/etc/network/interfaces.d/setup)
[ LINK ] network/if-post-down.d/wireless-tools (-> ../../../dotfiles/common/etc/network/if-post-down.d/wireless-tools)
[ LINK ] network/if-post-down.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-post-down.d/wpasupplicant)
[ LINK ] network/if-post-down.d/vde2 (-> ../../../dotfiles/common/etc/network/if-post-down.d/vde2)
[ LINK ] network/if-post-down.d/bridge (-> ../../../dotfiles/common/etc/network/if-post-down.d/bridge)
[ LINK ] network/if-pre-up.d/ethtool (-> ../../../dotfiles/common/etc/network/if-pre-up.d/ethtool)
[ LINK ] network/if-pre-up.d/wireless-tools (-> ../../../dotfiles/common/etc/network/if-pre-up.d/wireless-tools)
[ LINK ] network/if-pre-up.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-pre-up.d/wpasupplicant)
[ LINK ] network/if-pre-up.d/vde2 (-> ../../../dotfiles/common/etc/network/if-pre-up.d/vde2)
[ LINK ] network/if-pre-up.d/bridge (-> ../../../dotfiles/common/etc/network/if-pre-up.d/bridge)
[ LINK ] network/if-down.d/avahi-autoipd (-> ../../../dotfiles/common/etc/network/if-down.d/avahi-autoipd)
[ LINK ] network/if-down.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-down.d/wpasupplicant)
[ LINK ] network/if-down.d/bridge (-> ../../../dotfiles/common/etc/network/if-down.d/bridge)
[ LINK ] security/passwdqc.conf (-> ../../dotfiles/common/etc/security/passwdqc.conf)
[ LINK ] eixrc/00-eixrc (-> ../../dotfiles/common/etc/eixrc/00-eixrc)