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)