Source code for app.sources.upload
"""UploadSource — bytes live in the content-addressed blob store.
Materialization links/copies blobs into a working dir under their image
names so pycolmap sees a regular dir of `IMG_001.jpg, IMG_002.jpg, ...`
"""
from __future__ import annotations
import contextlib
import os
import shutil
from dataclasses import dataclass
from pathlib import Path
from app.core.errors import StorageError
from app.sources.base import MaterializedImage
from app.storage.blobs import BlobStore, get_blob_store
@dataclass
class UploadEntry:
name: str
blob_sha: str
[docs]
@dataclass
class UploadSource:
kind: str = "upload"
entries: list[UploadEntry] = None # type: ignore[assignment]
def __post_init__(self) -> None:
if self.entries is None:
self.entries = []
[docs]
def fingerprint(self) -> dict:
# Sorted by (name, sha) — deterministic.
sorted_entries = sorted((e.name, e.blob_sha) for e in self.entries)
return {"kind": self.kind, "entries": sorted_entries}
[docs]
def materialize(
self, into: Path, blob_store: BlobStore | None = None
) -> list[MaterializedImage]:
bs = blob_store or get_blob_store()
into.mkdir(parents=True, exist_ok=True)
out: list[MaterializedImage] = []
for entry in self.entries:
try:
src = bs.local_path(entry.blob_sha)
except StorageError:
raise StorageError(f"Blob missing: {entry.blob_sha}") from None
dst = into / entry.name
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
dst.unlink()
try:
# Hardlink first; falls back to copy across volumes / on Windows.
os.link(src, dst)
except OSError:
with contextlib.suppress(OSError):
shutil.copy2(src, dst)
out.append(MaterializedImage(name=entry.name, abs_path=dst, content_sha=entry.blob_sha))
return out