US08-03: Compose the Runtime and Mount the Library Safely (#98)

This commit was merged in pull request #98.
This commit is contained in:
2026-08-19 19:47:42 +02:00
parent 888d859e93
commit 1632544132
10 changed files with 1007 additions and 30 deletions

View File

@@ -11,9 +11,11 @@ from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Sequence
from photo_pipeline import path_policy
from photo_pipeline.config import Config
from photo_pipeline.services.app_lock import LegacyProcessActive, LibraryLock, LockHeld
from photo_pipeline.services.backup import BackupError, BackupService, migrate_with_backup
@@ -83,6 +85,18 @@ def main(argv: Sequence[str] | None = None) -> int:
config = Config.from_env()
config.database_path.parent.mkdir(parents=True, exist_ok=True)
# In a container the two roles that touch the library check their boundary before
# they take a lock or bind a port: the configured roots must name the mount paths,
# and a mismatch is cheaper to refuse than to discover at the first write (US08-03).
# Only in a container — on a host an unmounted root is an ordinary Tuesday (an
# archive medium that is not plugged in), and refusing to serve would take the
# offline half of the library away with it.
if args.command in ("serve", "worker") and path_policy.in_container():
refusal = path_policy.roots_refusal(config.library_roots, require_mount=True)
if refusal is not None:
print(refusal, file=sys.stderr)
return 5
if args.command == "migrate":
manifest = migrate_with_backup(config)
if manifest:
@@ -209,8 +223,6 @@ def main(argv: Sequence[str] | None = None) -> int:
lock.release()
return 0
import sys
import uvicorn
from photo_pipeline.api.app import ConfigurationRefused, create_app
@@ -238,8 +250,6 @@ def _acquire(lock: LibraryLock, *, allow_legacy: bool) -> int | None:
Returns an exit code to return, or ``None`` when the lock was acquired.
"""
import sys
try:
lock.acquire(allow_legacy=allow_legacy)
except LockHeld as error: