vignette/apps/api/app/upload_runtime.py
2026-08-29 23:58:33 +09:00

228 lines
9 KiB
Python

"""Configured public-avatar storage boundary used by API routes and health."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import Response
from starlette.staticfiles import StaticFiles
from .config import settings
from .upload_storage import (
CurrentAvatarReferenceProof,
PUBLIC_AVATAR_DIRECTORY,
UploadManifestProof,
UploadWriteFreezeGate,
assert_path_without_reparse,
assert_regular_file_without_reparse,
connected_database_target_sha256,
is_runtime_generated_public_avatar_relative_path,
public_avatar_url_to_relative_path,
public_avatar_relative_path_sha256,
read_decodable_public_avatar_object,
validate_current_avatar_references,
validate_upload_manifest,
)
def _configured_upload_root() -> Path:
configured = Path(settings.user_upload_dir)
if not configured.is_absolute():
if settings.user_upload_manifest_required:
raise RuntimeError("public USER_UPLOAD_DIR must be absolute")
configured = Path.cwd() / configured
if settings.user_upload_manifest_required:
try:
assert_path_without_reparse(configured, "public USER_UPLOAD_DIR")
except ValueError as exc:
raise RuntimeError(str(exc)) from exc
return configured.resolve(strict=False)
upload_root = _configured_upload_root()
upload_manifest_proof: UploadManifestProof | None = None
if settings.user_upload_manifest_required:
if not upload_root.is_dir():
raise RuntimeError("initialized public USER_UPLOAD_DIR does not exist")
if not settings.user_upload_manifest_path:
raise RuntimeError("USER_UPLOAD_MANIFEST_PATH is required")
if len(settings.user_upload_manifest_sha256) != 64:
raise RuntimeError("USER_UPLOAD_MANIFEST_SHA256 is required")
if not settings.user_upload_write_freeze_path:
raise RuntimeError("USER_UPLOAD_WRITE_FREEZE_PATH is required")
freeze_path = Path(settings.user_upload_write_freeze_path)
if not freeze_path.is_absolute():
raise RuntimeError("USER_UPLOAD_WRITE_FREEZE_PATH must be absolute")
upload_manifest_proof = validate_upload_manifest(
upload_root=upload_root,
manifest_path=Path(settings.user_upload_manifest_path),
expected_manifest_sha256=settings.user_upload_manifest_sha256,
expected_write_freeze_path=freeze_path,
expected_database_target_sha256=settings.public_runtime_db_target_sha256,
verify_preserved_objects=True,
reject_unbound_extras=False,
)
else:
upload_root.mkdir(parents=True, exist_ok=True)
public_avatar_root = upload_root / PUBLIC_AVATAR_DIRECTORY
if settings.user_upload_manifest_required:
if not public_avatar_root.is_dir():
raise RuntimeError("initialized public avatar directory does not exist")
try:
assert_path_without_reparse(
public_avatar_root,
"initialized public avatar directory",
)
except ValueError as exc:
raise RuntimeError(str(exc)) from exc
else:
public_avatar_root.mkdir(parents=True, exist_ok=True)
configured_freeze_path = (
Path(settings.user_upload_write_freeze_path)
if settings.user_upload_write_freeze_path
else None
)
upload_write_freeze_gate = UploadWriteFreezeGate(configured_freeze_path)
@dataclass(frozen=True, slots=True)
class RuntimeUploadDatabaseProof:
database_target_sha256: str
current_references: CurrentAvatarReferenceProof
async def validate_runtime_upload_database_state(
pool: Any,
) -> RuntimeUploadDatabaseProof:
"""Bind readiness to one repeatable-read snapshot from this API's pool."""
if upload_manifest_proof is None:
raise RuntimeError("public upload manifest proof is unavailable")
async with pool.acquire() as connection:
async with connection.transaction(isolation="repeatable_read", readonly=True):
database_target = await connected_database_target_sha256(connection)
if database_target != settings.public_runtime_db_target_sha256:
raise RuntimeError("connected public runtime database target drift")
await connection.execute(
"SELECT set_config('app.ai_context', '', true), "
"set_config('app.current_role', 'admin', true)"
)
rows = await connection.fetch(
"""
SELECT avatar_url
FROM app.app_user
WHERE avatar_url IS NOT NULL
AND btrim(avatar_url) LIKE '/uploads/%'
ORDER BY avatar_url
"""
)
current_references = validate_current_avatar_references(
upload_root=upload_root,
avatar_urls=[str(row["avatar_url"]) for row in rows],
initial_manifest=upload_manifest_proof,
expected_database_target_sha256=database_target,
)
return RuntimeUploadDatabaseProof(
database_target_sha256=database_target,
current_references=current_references,
)
def assert_public_avatar_url_exists(value: str) -> None:
if not value.startswith("/uploads/"):
return
relative_path = public_avatar_url_to_relative_path(value)
path_sha256 = public_avatar_relative_path_sha256(relative_path)
preserved_decode_by_path = dict(
upload_manifest_proof.preserved_path_decode_records
if upload_manifest_proof is not None
else ()
)
decode_valid = preserved_decode_by_path.get(path_sha256)
if decode_valid is False:
raise ValueError("public avatar URL object is forensic-only")
candidate = upload_root / Path(*relative_path.split("/"))
if decode_valid is True:
assert_regular_file_without_reparse(
candidate,
"public avatar URL object",
)
return
if not is_runtime_generated_public_avatar_relative_path(relative_path):
raise ValueError("public avatar URL object is not authorized")
read_decodable_public_avatar_object(
relative_path=relative_path,
path=candidate,
label="public avatar URL object",
)
class FlatPublicAvatarStaticFiles(StaticFiles):
"""Serve only one validated public avatar image, never nested/hidden extras."""
async def get_response(self, path: str, scope):
method = str(scope.get("method", "GET")).upper()
if method not in {"GET", "HEAD"}:
raise StarletteHTTPException(status_code=405)
try:
normalized = public_avatar_url_to_relative_path(
f"/uploads/profile-avatars/{path}"
)
filename = normalized.split("/", 1)[1]
path_sha256 = public_avatar_relative_path_sha256(normalized)
preserved_decode_by_path = dict(
upload_manifest_proof.preserved_path_decode_records
if upload_manifest_proof is not None
else ()
)
preserved_public_cache = {
record[0]: (record[1], record[2], record[3], record[4])
for record in (
upload_manifest_proof.preserved_records
if upload_manifest_proof is not None
else ()
)
}
decode_valid = preserved_decode_by_path.get(path_sha256)
if decode_valid is False:
raise ValueError("public avatar object is forensic-only")
preserved = preserved_public_cache.get(path_sha256)
if preserved is not None:
content_sha256, size_bytes, media_type, content = preserved
body = content if method == "GET" else b""
return Response(
content=body,
media_type=media_type,
headers={
"Content-Length": str(size_bytes),
"ETag": f'"{content_sha256}"',
"Cache-Control": "public, max-age=3600",
},
)
if decode_valid is True:
raise ValueError("validated public avatar cache is unavailable")
if not is_runtime_generated_public_avatar_relative_path(normalized):
raise ValueError("public avatar object is not authorized")
candidate = Path(self.directory) / filename
runtime_object = read_decodable_public_avatar_object(
relative_path=normalized,
path=candidate,
label="public avatar static object",
)
except (OSError, ValueError) as exc:
raise StarletteHTTPException(status_code=404) from exc
return Response(
content=runtime_object.content if method == "GET" else b"",
media_type=runtime_object.media_type,
headers={
"Content-Length": str(runtime_object.size_bytes),
"ETag": f'"{runtime_object.content_sha256}"',
"Cache-Control": "public, max-age=3600",
},
)