From d5e18175039a2ec15f4ee71851ee8b11d1011e22 Mon Sep 17 00:00:00 2001 From: menzel Date: Wed, 25 Feb 2026 10:18:40 +0100 Subject: [PATCH] making fix_dir_mtime.py more robust --- fix_dir_mtime.py | 65 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/fix_dir_mtime.py b/fix_dir_mtime.py index 3e6f457..45616fe 100755 --- a/fix_dir_mtime.py +++ b/fix_dir_mtime.py @@ -1,34 +1,71 @@ #!/usr/bin/env python3 - import os import sys from pathlib import Path -def update_dir_mtime(path: Path): +def stat_follow(p: Path): + """stat() that follows symlinks; returns None if target missing.""" + try: + return p.stat() # follows symlinks + except FileNotFoundError: + return None + + +def update_dir_mtime(path: Path, visited: set[tuple[int, int]]) -> float: + """ + Make directory mtime >= newest reachable content mtime. + Follows working symlinks; ignores broken ones. + Prevents cycles via inode tracking. + Returns the newest mtime found under 'path' (including itself after update). + """ + st = stat_follow(path) + if st is None: + return 0.0 + + key = (st.st_dev, st.st_ino) + if key in visited: + return st.st_mtime # cycle: stop here + visited.add(key) + + # If it's not a directory, just return its mtime. if not path.is_dir(): - return path.stat().st_mtime + return st.st_mtime - newest = 0 + newest = 0.0 - for child in path.iterdir(): - if child.is_dir(): - child_mtime = update_dir_mtime(child) - else: - child_mtime = child.stat().st_mtime + try: + for child in path.iterdir(): + # Follow working symlinks (child.stat() will fail if broken). + child_st = stat_follow(child) + if child_st is None: + continue # broken symlink or vanished entry -> ignore - newest = max(newest, child_mtime) + if ( + child.is_dir() + ): # this is True for symlink->dir as well (when target exists) + child_mtime = update_dir_mtime(child, visited) + else: + child_mtime = child_st.st_mtime - if newest > 0: + if child_mtime > newest: + newest = child_mtime + except PermissionError: + # If you hit protected folders, skip descending but still allow returning current mtime. + newest = 0.0 + + # Update this directory to reflect newest reachable content. + if newest > 0.0 and newest > st.st_mtime: os.utime(path, (newest, newest)) + st = stat_follow(path) or st - return max(newest, path.stat().st_mtime) + return max(newest, st.st_mtime) if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: fix_dir_mtime.py ") - sys.exit(1) + raise SystemExit(1) root = Path(sys.argv[1]).resolve() - update_dir_mtime(root) + update_dir_mtime(root, visited=set())