US08-03: Compose the Runtime and Mount the Library Safely (#98)
This commit was merged in pull request #98.
This commit is contained in:
@@ -15,9 +15,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Iterator
|
||||
|
||||
SUPPORTED_EXTENSIONS = {
|
||||
".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".tiff", ".tif"
|
||||
}
|
||||
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".tiff", ".tif"}
|
||||
EXCLUDED_DIR_NAMES = {"_IGNORE", ".@__thumb"}
|
||||
|
||||
|
||||
@@ -72,6 +70,63 @@ def resolve_in_roots(roots: Iterable[os.PathLike | str], path: os.PathLike | str
|
||||
raise PathPolicyError("path is outside the configured library roots")
|
||||
|
||||
|
||||
def in_container() -> bool:
|
||||
"""Whether this process is running inside a container image build of the app."""
|
||||
return Path("/.dockerenv").exists()
|
||||
|
||||
|
||||
def _under_mount(path: Path) -> bool:
|
||||
"""Whether ``path`` or one of its parents below ``/`` is a mounted filesystem."""
|
||||
current = path.resolve()
|
||||
while current != current.parent:
|
||||
if os.path.ismount(current):
|
||||
return True
|
||||
current = current.parent
|
||||
return False
|
||||
|
||||
|
||||
def roots_refusal(roots: Iterable[os.PathLike | str], *, require_mount: bool = False) -> str | None:
|
||||
"""Why the configured library roots cannot be worked with, or ``None`` (US08-03).
|
||||
|
||||
The roots name the directories this installation renames folders in, rewrites
|
||||
EXIF in, and archives from. Container path policy is the same problem as host
|
||||
path policy with one new failure mode: the configured roots must name the
|
||||
*container-side* mount paths. A host path configured inside a container is
|
||||
either absent or an ordinary directory of the image, so the library looks empty
|
||||
and the first write lands in the container's throwaway layer instead of in the
|
||||
library. That is worth refusing at startup, while an operator is still watching,
|
||||
rather than at the first write.
|
||||
|
||||
``require_mount`` is the container-only half of that: inside a container a real
|
||||
library arrives through a bind mount, so a root that is not on (or under) a
|
||||
mount point is not the library the deployment meant.
|
||||
|
||||
Writability is deliberately *not* checked: a bind mount's ownership is
|
||||
virtualised by Docker Desktop and Colima (it arrives as ``root:root``), so
|
||||
``os.access`` there is evidence about the virtio layer rather than about the
|
||||
library. A wrong UID/GID surfaces as a refused rename with the real errno, which
|
||||
is at least true; a refusal here would be false on two supported platforms.
|
||||
"""
|
||||
for root in roots:
|
||||
path = Path(root)
|
||||
if not path.exists():
|
||||
return (
|
||||
f"library root {path} does not exist: PHOTO_PIPELINE_LIBRARY_ROOTS must "
|
||||
"name paths that exist here, and in a container that means the mount path"
|
||||
)
|
||||
if not path.is_dir():
|
||||
return f"library root {path} is not a directory"
|
||||
if not os.access(path, os.R_OK | os.X_OK):
|
||||
return f"library root {path} is not readable by this process"
|
||||
if require_mount and not _under_mount(path):
|
||||
return (
|
||||
f"library root {path} is not on a mounted filesystem in this container: "
|
||||
"the library was not bind-mounted there, so PHOTO_PIPELINE_LIBRARY_ROOTS "
|
||||
"names a directory of the image rather than the library"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def iter_supported_files(root: os.PathLike | str) -> Iterator[Path]:
|
||||
"""Yield supported, non-excluded files under ``root`` in deterministic order.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user