Autodeploy / autodeploy (push) Successful in 13s
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>
161 lines
5.4 KiB
Python
Executable File
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()
|