diff --git a/app.py b/app.py index 400b6e8..b6a87a2 100644 --- a/app.py +++ b/app.py @@ -155,6 +155,7 @@ async def local_then_core(request: Request, call_next): "/reslevis/updateAlarm", "/reslevis/updateCoreSettings", "/reslevis/updateUserPreferences", + "/reslevis/uploadFloorMap", } # only proxy CRUD for Reslevis (change prefix or methods if needed) if ( diff --git a/config_env.py b/config_env.py index b208d5a..0a462fd 100644 --- a/config_env.py +++ b/config_env.py @@ -167,3 +167,12 @@ BLE_AI_MAPS_DIR = os.getenv( "/data/service/ble-ai-localizer/data/maps", ) +RESLEVIS_MAPS_DIR = os.getenv( + "RESLEVIS_MAPS_DIR", + str(Path(__file__).resolve().parent / "assets" / "maps"), +) + +RESLEVIS_MAPS_PUBLIC_PATH = os.getenv( + "RESLEVIS_MAPS_PUBLIC_PATH", + "assets/maps", +).strip("/") diff --git a/routes/reslevis.py b/routes/reslevis.py index fe398ff..8c5bb95 100644 --- a/routes/reslevis.py +++ b/routes/reslevis.py @@ -1,11 +1,13 @@ import asyncio import json +import os +import tempfile from urllib.parse import urlencode -from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile import httpx import config_env -from typing import List, Optional +from typing import Any, Dict, List, Optional from schemas.reslevis import ( BuildingItem, @@ -26,6 +28,7 @@ from schemas.reslevis import ( GuiConfigItem, UserPreferencesItem, UserPreferencesUpdateItem, + FloorMapUploadResponseItem, CoreSettingsItem, CoreSettingsUpdateItem, ) @@ -54,6 +57,7 @@ ALERTS_CORE_BASE_URL = "http://localhost:1902" TRACKS_CORE_BASE_URL = "http://localhost:1902" SETTINGS_CORE_BASE_URL = "http://127.0.0.1:1902" CORE_TIMEOUT = 2.0 # secondi +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" async def sync_core_get(request: Request) -> None: if request.method != "GET": @@ -171,6 +175,130 @@ def _uid_from_claims(claims: dict) -> str: raise HTTPException(status_code=401, detail="User identity not found in token") return str(uid) + +def _floor_maps_index_path() -> str: + return os.path.join(config_env.RESLEVIS_MAPS_DIR, "maps.json") + + +def _read_floor_maps_index() -> Dict[str, Any]: + path = _floor_maps_index_path() + if not os.path.isfile(path): + return {"items": [], "count": 0} + + try: + with open(path, "r", encoding="utf-8") as fp: + data = json.load(fp) + except (OSError, ValueError): + return {"items": [], "count": 0} + + if not isinstance(data, dict): + return {"items": [], "count": 0} + + items = data.get("items") + if not isinstance(items, list): + items = [] + return {"items": items, "count": len(items)} + + +def _write_floor_maps_index(index: Dict[str, Any]) -> None: + maps_dir = config_env.RESLEVIS_MAPS_DIR + os.makedirs(maps_dir, exist_ok=True) + + index["count"] = len(index.get("items") or []) + payload = json.dumps(index, ensure_ascii=False, indent=2) + + temp_name = None + try: + with tempfile.NamedTemporaryFile("w", dir=maps_dir, delete=False, encoding="utf-8") as tmp: + tmp.write(payload) + tmp.flush() + os.fsync(tmp.fileno()) + temp_name = tmp.name + + os.replace(temp_name, _floor_maps_index_path()) + try: + os.chmod(_floor_maps_index_path(), 0o664) + except OSError: + pass + except OSError as exc: + if temp_name and os.path.exists(temp_name): + try: + os.remove(temp_name) + except OSError: + pass + raise HTTPException(status_code=500, detail="Unable to update maps.json") from exc + + +def _same_floor(row: Dict[str, Any], floor: int) -> bool: + try: + return int(row.get("floor")) == floor + except (TypeError, ValueError): + return False + + +def _floor_sort_key(row: Dict[str, Any]) -> int: + try: + return int(row.get("floor")) + except (TypeError, ValueError): + return 0 + + +def _upsert_floor_map_record(floor: int, name: str, metadata: Dict[str, Any]) -> None: + index = _read_floor_maps_index() + item = { + "floor": floor, + "name": name, + "mime_type": "image/png", + "metadata": metadata, + } + + items = [r for r in index["items"] if isinstance(r, dict) and not _same_floor(r, floor)] + items.append(item) + items.sort(key=_floor_sort_key) + index["items"] = items + _write_floor_maps_index(index) + + +def _public_map_path(name: str) -> str: + public_path = config_env.RESLEVIS_MAPS_PUBLIC_PATH + return f"{public_path}/{name}" if public_path else name + + +def _validate_map_target(maps_dir: str, name: str) -> str: + maps_dir_abs = os.path.abspath(maps_dir) + target_path = os.path.abspath(os.path.join(maps_dir_abs, name)) + if os.path.commonpath([maps_dir_abs, target_path]) != maps_dir_abs: + raise HTTPException(status_code=400, detail="Invalid map filename") + return target_path + + +async def _write_upload_file(upload: UploadFile, target_path: str) -> None: + maps_dir = os.path.dirname(target_path) + temp_name = None + try: + with tempfile.NamedTemporaryFile("wb", dir=maps_dir, delete=False) as tmp: + temp_name = tmp.name + while True: + chunk = await upload.read(1024 * 1024) + if not chunk: + break + tmp.write(chunk) + tmp.flush() + os.fsync(tmp.fileno()) + + os.replace(temp_name, target_path) + try: + os.chmod(target_path, 0o664) + except OSError: + pass + except OSError as exc: + if temp_name and os.path.exists(temp_name): + try: + os.remove(temp_name) + except OSError: + pass + raise HTTPException(status_code=500, detail="Unable to save floor map") from exc + CORE_GET_SYNC = { "/reslevis/getGateways": (gateway_repo, _normalize_gateway), "/reslevis/getZones": (zone_repo, _normalize_zone), @@ -645,6 +773,59 @@ def updateUserPreferences( return user_preferences_repo.update(uid, item) +@router.post( + "/uploadFloorMap", + response_model=FloorMapUploadResponseItem, + tags=["Reslevis"], + dependencies=[Depends(get_current_user)], +) +async def uploadFloorMap( + floor: int = Form(...), + file: UploadFile = File(...), + pixel_ratio: float = Form(1), + calibrated: bool = Form(False), + origin_x: int = Form(0), + origin_y: int = Form(0), + grid_size: int = Form(50), +): + if pixel_ratio <= 0: + raise HTTPException(status_code=400, detail="pixel_ratio must be greater than 0") + if grid_size <= 0: + raise HTTPException(status_code=400, detail="grid_size must be greater than 0") + + content_type = (file.content_type or "").split(";")[0].strip().lower() + if content_type and content_type != "image/png": + raise HTTPException(status_code=400, detail="Only PNG files are allowed") + + signature = await file.read(len(PNG_SIGNATURE)) + if signature != PNG_SIGNATURE: + raise HTTPException(status_code=400, detail="Only PNG files are allowed") + await file.seek(0) + + maps_dir = config_env.RESLEVIS_MAPS_DIR + os.makedirs(maps_dir, exist_ok=True) + + name = f"floor_{floor}.png" + target_path = _validate_map_target(maps_dir, name) + await _write_upload_file(file, target_path) + + metadata = { + "pixel_ratio": pixel_ratio, + "calibrated": calibrated, + "origin": [origin_x, origin_y], + "grid_size": grid_size, + } + _upsert_floor_map_record(floor, name, metadata) + + return { + "floor": floor, + "name": name, + "path": _public_map_path(name), + "mime_type": "image/png", + "metadata": metadata, + } + + @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postGuiConfig(item: GuiConfigItem): gui_config_repo.add(item) diff --git a/schemas/reslevis.py b/schemas/reslevis.py index 8cab200..635bc6e 100644 --- a/schemas/reslevis.py +++ b/schemas/reslevis.py @@ -266,6 +266,15 @@ class CalibrationMetadata(BaseModel): origin: Tuple[int, int] grid_size: int + +class FloorMapUploadResponseItem(BaseModel): + floor: int + name: str + path: str + mime_type: str + metadata: CalibrationMetadata + + #??? Da verificate ??? class DownloadFileImmage(): name : str