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)
|