Files
public-tools/fix_dir_mtime.py
T

72 lines
2.1 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import sys
from pathlib import 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 st.st_mtime
newest = 0.0
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
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 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, st.st_mtime)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: fix_dir_mtime.py <directory>")
raise SystemExit(1)
root = Path(sys.argv[1]).resolve()
update_dir_mtime(root, visited=set())