|
|
|
@@ -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) |
|
|
|
|