Files
operation-bin/refresh_home_links
T
lemke_handClaude Sonnet 5 2d8b6e046f
Autodeploy / autodeploy (push) Successful in 13s
Recover work lost in this morning's autodeploy re-clone
The 07:44 autodeploy pull of this repo re-cloned from scratch and wiped
everything that wasn't committed. Restored from the 07:05 hourly NFS
snapshot (.snapshot/hourly.2026-09-07_0705), the last one taken before
the wipe:

- eco-status-server, eco-status-server-install-user-service: switch from
  tracked copies to symlinks into the canonical eco repo checkout at
  /sf/bernina/code/gac-bernina/eco/scripts/, so edits there take effect
  immediately without a redeploy step.
- eco-dev-pull-new-version: small helper to update that eco checkout.
- refresh_home_links: interactive helper to clear local files blocking
  linux_home_links.yaml link:/copy: targets before restarting
  create-automatic-links-in-home.service.
- .vscode/settings.json: restore local Peacock/color-theme customization.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 13:20:07 +02:00

161 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Check the link:/copy: entries in ~/network_drives/home/linux_home_links.yaml
for local files that would block create-automatic-links-in-home.service, let
the user resolve any conflicts, then restart the service.
For each entry the effective destination/source paths are resolved exactly
the way /opt/pli/bin/create-automatic-links-in-home resolves them:
- link: destination = ~/<key> source = (destination.parent / to)
- copy: destination = ~/<key> source = ~/<from>
A destination is "free" if nothing exists there, or if it is already a
symlink pointing at the resolved source. Otherwise its content is diffed
against the source. An empty diff is auto-resolved (the blocking file is
simply removed, since it is byte-identical to the source it would be
replaced by). A non-empty diff is shown to the user, who can:
d - delete the blocking file (source wins)
c - copy the blocking file to the source location, then delete it (local
copy wins, and gets pushed back to the shared source)
a - abort without touching anything or restarting the service
"""
import difflib
import filecmp
import os
import subprocess
import sys
from pathlib import Path
import yaml
HOME = Path.home()
CONFIG_CANDIDATES = [
HOME / "network_drives/home/linux_home_links.yaml",
HOME / "network_drives/home/.linux_home_links.yaml",
]
def load_config():
for path in CONFIG_CANDIDATES:
if path.is_file():
with path.open() as f:
return path, (yaml.safe_load(f) or {})
print("error: no linux_home_links.yaml found under ~/network_drives/home/", file=sys.stderr)
sys.exit(1)
def show_diff(dest, source, dest_text, source_text):
diff = difflib.unified_diff(
source_text.splitlines(keepends=True),
dest_text.splitlines(keepends=True),
fromfile=str(source),
tofile=str(dest),
)
sys.stdout.writelines(diff)
def read_text_safely(path):
try:
return path.read_text(errors="replace")
except (UnicodeDecodeError, OSError):
return None
def resolve_conflict(dest: Path, source: Path) -> bool:
"""Return True if resolved (or aborted-through, caller checks), False if user aborted."""
print(f"\nConflict: {dest}")
print(f" blocks link/copy from source: {source}")
dest_text = read_text_safely(dest) if dest.is_file() else None
source_text = read_text_safely(source) if source.is_file() else None
if dest_text is not None and source_text is not None:
show_diff(dest, source, dest_text, source_text)
else:
print(" (binary or unreadable content, diff not shown)")
while True:
choice = input(" [d]elete blocking file / [c]opy it to source / [a]bort? ").strip().lower()
if choice == "d":
dest.unlink()
print(f" deleted {dest}")
return True
if choice == "c":
source.parent.mkdir(parents=True, exist_ok=True)
data = dest.read_bytes()
source.write_bytes(data)
dest.unlink()
print(f" copied {dest} -> {source}, then removed {dest}")
return True
if choice == "a":
return False
print(" please answer d, c, or a")
def check_destination(dest: Path, source: Path, resolved_target_str: str = None) -> bool:
"""Return True if clear (or successfully resolved), False if the user aborted."""
if dest.is_symlink():
if resolved_target_str is not None and os.readlink(dest) == resolved_target_str:
return True # already correctly linked
# symlink pointing elsewhere (or broken) - treat its (possibly
# unreadable) target as the blocking content
if not source.exists():
print(f"\nWarning: source missing for {dest} -> {source}; leaving symlink as-is")
return True
if dest.exists() and filecmp.cmp(dest, source, shallow=False):
dest.unlink()
print(f"identical, removed stale symlink: {dest}")
return True
return resolve_conflict(dest, source)
if not dest.exists():
return True # free
# regular file (or dir) sitting where a link/copy should go
if not source.exists():
print(f"\nWarning: source missing for {dest} -> {source}; leaving blocking file as-is")
return True
if dest.is_file() and source.is_file() and filecmp.cmp(dest, source, shallow=False):
dest.unlink()
print(f"identical, removed redundant file: {dest}")
return True
return resolve_conflict(dest, source)
def main():
config_path, config = load_config()
print(f"using {config_path}")
ok = True
for key, entry in (config.get("link") or {}).items():
dest = HOME / key
to = entry["to"]
source = (dest.parent / to).resolve()
if not check_destination(dest, source, resolved_target_str=to):
ok = False
for key, entry in (config.get("copy") or {}).items():
dest = HOME / key
source = (HOME / entry["from"]).resolve()
if not check_destination(dest, source):
ok = False
if not ok:
print("\nAborted: not all conflicts were resolved, service not restarted.")
sys.exit(1)
print("\nAll link/copy targets are free or clear. Restarting service...")
subprocess.run(
["systemctl", "--user", "restart", "create-automatic-links-in-home.service"],
check=True,
)
print("done.")
if __name__ == "__main__":
main()