face-intel / storage /artifacts.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
2.78 kB
"""
Filesystem artifact store.
Responsible for saving uploaded source images, generated annotated
images, and any other binary artifacts. Returns relative paths so the
API can construct download URLs without coupling to the filesystem layout.
"""
from __future__ import annotations
import time
from pathlib import Path
from typing import Optional
import cv2
import numpy as np
from loguru import logger
class ArtifactStore:
"""Saves and reads image artifacts under a root directory."""
def __init__(self, root: Path) -> None:
self._root = Path(root)
self._uploads = self._root / "uploads"
self._generated = self._root / "generated"
self._uploads.mkdir(parents=True, exist_ok=True)
self._generated.mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------------ #
# Uploads (source images from users)
# ------------------------------------------------------------------ #
def save_upload(self, img: np.ndarray, prefix: str = "upload") -> Path:
"""Save an uploaded image. Returns absolute path."""
path = self._uploads / f"{prefix}_{int(time.time() * 1000)}.jpg"
cv2.imwrite(str(path), img)
return path
def save_upload_bytes(self, data: bytes, prefix: str = "upload",
suffix: str = ".jpg") -> Path:
path = self._uploads / f"{prefix}_{int(time.time() * 1000)}{suffix}"
path.write_bytes(data)
return path
# ------------------------------------------------------------------ #
# Generated artifacts (annotated images, crops, montages)
# ------------------------------------------------------------------ #
def save_generated(self, img: np.ndarray, prefix: str = "gen") -> Path:
path = self._generated / f"{prefix}_{int(time.time() * 1000)}.jpg"
cv2.imwrite(str(path), img)
return path
# ------------------------------------------------------------------ #
# Lookup
# ------------------------------------------------------------------ #
def resolve(self, relative_path: str) -> Optional[Path]:
"""Resolve a relative path to absolute, with root-jail safety."""
target = (self._root / relative_path).resolve()
try:
target.relative_to(self._root.resolve())
except ValueError:
logger.warning(f"Path traversal attempt blocked: {relative_path}")
return None
if not target.exists():
return None
return target
@property
def root(self) -> Path:
return self._root
@property
def uploads_dir(self) -> Path:
return self._uploads
@property
def generated_dir(self) -> Path:
return self._generated