Publish snippets and Markdown posts through authenticated uploads

This commit is contained in:
2026-09-19 15:34:17 +03:30
parent 17f1a8ef39
commit e42646e09e
14 changed files with 553 additions and 15 deletions
+41 -5
View File
@@ -10,6 +10,7 @@ import sys
import time
from app import atomic_json
from posts import publish_post
log = logging.getLogger("publisher")
@@ -48,6 +49,7 @@ class Publisher:
for job in jobs:
self.state(job, "publishing")
try:
outcomes = {}
if not (self.repo / ".git").is_dir():
self.git("clone", "--branch", self.branch, "--single-branch", "--",
self.remote, str(self.repo), directory=self.work)
@@ -56,9 +58,31 @@ class Publisher:
self.git("reset", "--hard", f"origin/{self.branch}")
self.git("config", "user.name", os.environ.get("PHOTO_GIT_NAME", "Photo publisher"))
self.git("config", "user.email", os.environ.get("PHOTO_GIT_EMAIL", "photos@theread.me"))
# Prepending oldest first leaves the newest snippet at the top.
jobs.sort(key=lambda job: (json.loads((self.data / "submissions" / job / "manifest.json").read_text())["date"], job))
for job in jobs:
submission = self.data / "submissions" / job
manifest = json.loads((submission / "manifest.json").read_text())
if manifest.get("kind") == "post":
entry, url, action, identity = publish_post(self.repo, submission, manifest)
self.git("add", "--", str(entry))
outcomes[job] = dict(url=url, action=action, post_id=identity)
continue
if manifest.get("kind") == "snippet":
entry = self.repo / "snippets.md"
existing = entry.read_text()
marker = "<!-- uploaded-snippets -->"
if marker not in existing:
raise OSError("snippets.md is missing its upload insertion marker")
if f'<div id="snippet-{job}"></div>' not in existing:
entry.write_text(existing.replace(marker, marker + "\n\n" +
(submission / "entry.md").read_text(), 1))
images = Path("img/snippets/uploads") / job
if manifest["count"]:
shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True)
self.git("add", "--", str(images))
self.git("add", "--", "snippets.md")
continue
entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md"
images = Path("img/arts/uploads") / job
(self.repo / entry).parent.mkdir(parents=True, exist_ok=True)
@@ -66,18 +90,30 @@ class Publisher:
shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True)
self.git("add", "--", str(entry), str(images))
if self.git("diff", "--cached", "--name-only"):
self.git("commit", "-m", f"Publish {len(jobs)} photo submission(s)")
self.git("commit", "-m", f"Publish {len(jobs)} submission(s)")
self.git("push", "origin", f"HEAD:refs/heads/{self.branch}")
commit = self.git("rev-parse", "HEAD")
for job in jobs:
self.state(job, "pushed", commit=commit)
self.state(job, "pushed", commit=commit, **outcomes.get(job, {}))
# A subsequent revision replaces the old one in the rendered site.
for job, outcome in outcomes.items():
job_date = json.loads((self.data / "submissions" / job / "manifest.json").read_text())["date"]
for status_path in (self.data / "status").glob("*.json"):
previous = json.loads(status_path.read_text())
if status_path.stem == job or previous.get("post_id") != outcome["post_id"]:
continue
old_manifest = self.data / "submissions" / status_path.stem / "manifest.json"
if old_manifest.exists() and json.loads(old_manifest.read_text())["date"] < job_date:
previous.update(status="superseded", replaced_by=job)
atomic_json(status_path, previous)
log.info("Pushed %s submission(s) at %s", len(jobs), commit)
except (OSError, subprocess.SubprocessError) as error:
except (OSError, ValueError, subprocess.SubprocessError) as error:
log.error("Git publication failed: %s", error)
if isinstance(error, subprocess.CalledProcessError):
log.error("%s", error.stderr)
for job in jobs:
self.state(job, "failed", error="Git push failed; check publisher logs and retry.")
self.state(job, "failed", error=str(error) if isinstance(error, ValueError) else
"Git push failed; check publisher logs and retry.")
raise
def run(self, once=False):
@@ -86,7 +122,7 @@ class Publisher:
while True:
try:
self.publish()
except (OSError, subprocess.SubprocessError):
except (OSError, ValueError, subprocess.SubprocessError):
if once:
raise
if once: