import re
from datetime import datetime, timezone
from typing import Optional
from pyinfra.api import FactBase
from typing_extensions import override
class File(FactBase[Optional[dict]]):
"""
Returns information about a file.
Returns:
dict with keys: user, group, mode, size, mtime, atime, ctime
None if file doesn't exist
False if path exists but is not a file
"""
@override
def command(self, path: str):
# Linux stat command
return (
f"stat -c 'user=%U group=%G mode=%A atime=%X mtime=%Y "
f"ctime=%Z size=%s %N' {path} 2>/dev/null || "
# BSD stat command (fallback)
f"stat -f 'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m "
f"ctime=%c size=%z %N%SY' {path}"
)
@override
def process(self, output: list[str]) -> Optional[dict]:
if not output:
return None
line = output[0]
# Parse stat output
pattern = (
r"user=(.*) group=(.*) mode=(.*) "
r"atime=(-?[0-9]*) mtime=(-?[0-9]*) ctime=(-?[0-9]*) "
r"size=([0-9]*) (.*)"
)
match = re.match(pattern, line)
if not match:
return None
user, group, mode, atime, mtime, ctime, size, name = match.groups()
# Convert timestamps to datetime objects
def parse_time(ts: str) -> Optional[datetime]:
try:
return datetime.fromtimestamp(int(ts), timezone.utc)
except (ValueError, TypeError):
return None
return {
"user": user,
"group": group,
"mode": self._parse_mode(mode),
"size": int(size),
"atime": parse_time(atime),
"mtime": parse_time(mtime),
"ctime": parse_time(ctime),
}
def _parse_mode(self, mode: str) -> int:
"""Convert rwxrwxrwx to octal."""
# Implementation details...
pass