republisher/repub/postprocessing.py

48 lines
1.4 KiB
Python
Raw Normal View History

2026-05-27 10:57:21 +02:00
from __future__ import annotations
import os
from contextlib import suppress
from pathlib import Path
from xml.etree import ElementTree
from repub.config import feed_output_path, staged_feed_output_path
def publish_staged_feed(*, out_dir: Path, feed_slug: str) -> Path:
staged_path = staged_feed_output_path(out_dir=out_dir, feed_slug=feed_slug)
public_path = feed_output_path(out_dir=out_dir, feed_slug=feed_slug)
public_path.parent.mkdir(parents=True, exist_ok=True)
_validate_staged_feed(staged_path)
_fsync_file(staged_path)
os.replace(staged_path, public_path)
_fsync_directory(public_path.parent)
return public_path
def _fsync_file(path: Path) -> None:
with path.open("rb") as handle:
os.fsync(handle.fileno())
def _validate_staged_feed(path: Path) -> None:
try:
root = ElementTree.parse(path).getroot()
except ElementTree.ParseError as error:
raise ValueError(f"Staged feed is not well-formed XML: {path}") from error
if root.tag != "rss":
raise ValueError(f"Staged feed is not an RSS document: {path}")
if root.find("channel") is None:
raise ValueError(f"Staged feed is missing an RSS channel: {path}")
def _fsync_directory(path: Path) -> None:
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
with suppress(OSError):
fd = os.open(path, flags)
try:
os.fsync(fd)
finally:
os.close(fd)