"""Content-addressed blob store. A blob id is ```` (e.g. ``3fa1….webp``); the file lives at ``blobs/3f/a1/``. The extension is part of the id so nginx can serve blobs with the right Content-Type straight from disk. """ import hashlib import os import re import shutil import tempfile from pathlib import Path from app.config import get_settings BLOB_ID_RE = re.compile(r"^[0-9a-f]{64}(\.[a-z0-9]{1,8})?$") _CHUNK = 1 << 20 def blob_path(blob_id: str) -> Path: if not BLOB_ID_RE.match(blob_id): raise ValueError(f"bad blob id {blob_id!r}") return get_settings().blobs_dir / blob_id[:2] / blob_id[2:4] / blob_id def _finalize(tmp: Path, digest: str, ext: str) -> tuple[str, int]: blob_id = digest + ext.lower() dest = blob_path(blob_id) size = tmp.stat().st_size if dest.exists(): tmp.unlink() else: dest.parent.mkdir(parents=True, exist_ok=True) tmp.chmod(0o644) # mkstemp creates 0600; nginx serves blobs as another user try: os.replace(tmp, dest) except OSError: # Another import may have published the same content since exists(). # Windows can refuse replacement while that file is being accessed. if not dest.is_file() or dest.stat().st_size != size: raise tmp.unlink() return blob_id, size def _tmp_file() -> Path: root = get_settings().blobs_dir / "tmp" root.mkdir(parents=True, exist_ok=True) fd, name = tempfile.mkstemp(dir=root) os.close(fd) return Path(name) def put_file(src: Path, ext: str, move: bool = False) -> tuple[str, int]: """Store a file, returning (blob id, size). ``move`` consumes ``src``.""" h = hashlib.sha256() with src.open("rb") as f: while chunk := f.read(_CHUNK): h.update(chunk) tmp = _tmp_file() if move: shutil.move(src, tmp) else: shutil.copyfile(src, tmp) return _finalize(tmp, h.hexdigest(), ext) def put_bytes(data: bytes, ext: str) -> tuple[str, int]: tmp = _tmp_file() tmp.write_bytes(data) return _finalize(tmp, hashlib.sha256(data).hexdigest(), ext) def read_bytes(blob_id: str) -> bytes: return blob_path(blob_id).read_bytes()