import os from pathlib import Path from typing import List from ..database import DATA_DIR APP_MODE_RAW = os.environ.get("APP_MODE", "").strip().lower() _SPACE_ID = os.environ.get("SPACE_ID", "").strip().lower() def is_hf_hosted() -> bool: return bool(_SPACE_ID) def _is_dda_mode() -> bool: if APP_MODE_RAW == "legacy": return False if APP_MODE_RAW == "dda": return True # APP_MODE unset: auto-enable on satdetect-dev only (production satdetect stays legacy) return _SPACE_ID.endswith("/satdetect-dev") def get_public_base_url() -> str: """Public URL for deep links (email, share). Override with PUBLIC_BASE_URL.""" explicit = os.environ.get("PUBLIC_BASE_URL", "").strip().rstrip("/") if explicit: return explicit if _SPACE_ID: return f"https://{_SPACE_ID.replace('/', '-')}.hf.space" return "http://localhost:7860" IS_DDA_MODE = _is_dda_mode() APP_MODE = APP_MODE_RAW or ("dda" if IS_DDA_MODE else "legacy") # Project root: change_detection_webapp/ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent def get_writable_library_root() -> Path: """Persistent folder for uploads (HF: /home/appuser/data/library_sources).""" return (DATA_DIR / "library_sources").resolve() def get_storage_root() -> Path: """Tree library root (doc: root_directory / STORAGE_ROOT).""" explicit = os.environ.get("STORAGE_ROOT", "").strip() if explicit: return Path(explicit).resolve() if os.environ.get("LOCAL_LIBRARY_ROOT"): return Path(os.environ["LOCAL_LIBRARY_ROOT"]).resolve() return get_writable_library_root() def get_library_roots() -> List[Path]: """Folders scanned for library images (tree storage root).""" roots: List[Path] = [] sr = get_storage_root() if sr not in roots: roots.append(sr) if os.environ.get("LOCAL_LIBRARY_ROOT"): p = Path(os.environ["LOCAL_LIBRARY_ROOT"]).resolve() if p not in roots: roots.append(p) if is_hf_hosted(): wr = get_writable_library_root() if wr not in roots: roots.append(wr) for candidate in (get_writable_library_root(), PROJECT_ROOT / "library_sources"): resolved = candidate.resolve() if resolved not in roots: roots.append(resolved) return roots # Primary root (shown in UI) — first entry from get_library_roots() LOCAL_LIBRARY_ROOT = get_library_roots()[0] LIBRARY_DIR = DATA_DIR / "library" THUMBS_DIR = LIBRARY_DIR / "thumbs" PREVIEWS_DIR = LIBRARY_DIR / "previews" LOCAL_THUMB_CACHE = DATA_DIR / "library_cache" / "thumbs" # Library upload limit (default 15 GB; override with MAX_GEOTIFF_MB / MAX_IMAGE_MB) _MAX_UPLOAD_MB_DEFAULT = "15360" # 15 * 1024 MAX_GEOTIFF_BYTES = int(os.environ.get("MAX_GEOTIFF_MB", _MAX_UPLOAD_MB_DEFAULT)) * 1024 * 1024 # PNG/JPEG use the same cap unless MAX_IMAGE_MB is set separately MAX_IMAGE_BYTES = int(os.environ.get("MAX_IMAGE_MB", _MAX_UPLOAD_MB_DEFAULT)) * 1024 * 1024 ALLOWED_EXTENSIONS = {".tif", ".tiff", ".png", ".jpg", ".jpeg"} def ensure_library_dirs() -> None: for root in get_library_roots(): try: root.mkdir(parents=True, exist_ok=True) except OSError: pass for d in (LIBRARY_DIR, THUMBS_DIR, PREVIEWS_DIR, LOCAL_THUMB_CACHE): try: d.mkdir(parents=True, exist_ok=True) except OSError: pass def ensure_local_year_folders() -> None: """Create default year folders under each library root if missing.""" from datetime import datetime current = datetime.now().year for root in get_library_roots(): for year in (current - 1, current, current + 1): try: (root / str(year)).mkdir(parents=True, exist_ok=True) except OSError: pass def max_upload_bytes_for_extension(ext: str) -> int: if ext in (".tif", ".tiff"): return MAX_GEOTIFF_BYTES return MAX_IMAGE_BYTES def geotiff_io_available() -> bool: try: import rasterio # noqa: F401 return True except ImportError: return False def get_detection_max_side() -> int: """Max pixel dimension for GeoTIFF load + detection pipeline (higher = sharper, more RAM).""" from ..detection_config import get_detection_max_side as _central return _central()