feat: enable remote SDK and resilient device orchestration
This commit is contained in:
1 parent
3cc711df51
commit
ddfa420945
110 files changed
+14450
-12954
No files matched your search
@@ -12,11 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
|
||||
from artemis.config import IMAGES_DIR, WORKSPACE_ROOT
|
||||
from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
|
||||
|
||||
try:
|
||||
from admin_console.database.repositories.session_repository import session_repo
|
||||
@@ -27,6 +28,48 @@ except ImportError:
|
||||
|
||||
router = APIRouter(tags=["media"])
|
||||
|
||||
_VIDEO_MEDIA_TYPES = {
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".mkv": "video/x-matroska",
|
||||
}
|
||||
|
||||
|
||||
def _allowed_media_roots() -> list[Path]:
|
||||
"""Directories the media endpoints are permitted to serve from."""
|
||||
roots = []
|
||||
for root in (WORKSPACE_ROOT, TRACES_PATH):
|
||||
try:
|
||||
roots.append(Path(root).resolve())
|
||||
except OSError:
|
||||
continue
|
||||
return roots
|
||||
|
||||
|
||||
def _resolve_media_path(raw_path: str, allowed_suffixes: set[str]) -> Path:
|
||||
"""Resolve a client-supplied path strictly inside the allowed media roots.
|
||||
|
||||
Every candidate is fully resolved (symlinks, `..`, mixed separators) and
|
||||
then re-checked against the allowed roots and an extension allowlist, so a
|
||||
crafted URL can never address source code, databases, or dotfiles.
|
||||
"""
|
||||
roots = _allowed_media_roots()
|
||||
candidate = Path(raw_path)
|
||||
attempts = [candidate] if candidate.is_absolute() else [root / candidate for root in roots]
|
||||
for attempt in attempts:
|
||||
try:
|
||||
resolved = attempt.resolve(strict=True)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if not resolved.is_file():
|
||||
continue
|
||||
if not any(resolved.is_relative_to(root) for root in roots):
|
||||
continue
|
||||
if resolved.suffix.lower() not in allowed_suffixes:
|
||||
raise HTTPException(status_code=403, detail="File type is not served by the media API.")
|
||||
return resolved
|
||||
raise HTTPException(status_code=404, detail="Media file not found")
|
||||
|
||||
|
||||
@router.get("/admin", response_class=HTMLResponse)
|
||||
@router.get("/debug", response_class=HTMLResponse)
|
||||
@@ -47,8 +90,12 @@ async def get_admin_index():
|
||||
async def get_image(image_name: str):
|
||||
if not image_name.endswith(".jpg"):
|
||||
image_name += ".jpg"
|
||||
image_path = IMAGES_DIR / image_name
|
||||
if not image_path.exists():
|
||||
try:
|
||||
images_root = IMAGES_DIR.resolve()
|
||||
image_path = (images_root / image_name).resolve(strict=True)
|
||||
except (OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
if not image_path.is_file() or not image_path.is_relative_to(images_root):
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
return FileResponse(image_path, media_type="image/jpeg")
|
||||
@@ -56,34 +103,18 @@ async def get_image(image_name: str):
|
||||
|
||||
@router.get("/videos/{video_path:path}")
|
||||
async def get_video(video_path: str):
|
||||
path = Path(video_path)
|
||||
if not path.exists():
|
||||
if not path.is_absolute():
|
||||
candidate = WORKSPACE_ROOT / path
|
||||
if candidate.exists():
|
||||
path = candidate
|
||||
else:
|
||||
candidate_root = Path("/" + video_path.lstrip("/"))
|
||||
if candidate_root.exists():
|
||||
path = candidate_root
|
||||
else:
|
||||
path = candidate
|
||||
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Video not found at {path}")
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
media_type = "video/mp4"
|
||||
if suffix == ".webm":
|
||||
media_type = "video/webm"
|
||||
elif suffix == ".mkv":
|
||||
media_type = "video/x-matroska"
|
||||
|
||||
return FileResponse(path, media_type=media_type)
|
||||
path = _resolve_media_path(video_path, set(_VIDEO_MEDIA_TYPES))
|
||||
return FileResponse(path, media_type=_VIDEO_MEDIA_TYPES[path.suffix.lower()])
|
||||
|
||||
|
||||
@router.get("/api/sessions/{session_id}/video")
|
||||
async def get_session_video(session_id: str):
|
||||
# Blocking work (sqlite, filesystem scan, possible ffmpeg conversion) runs
|
||||
# off the event loop.
|
||||
return await asyncio.to_thread(_get_session_video_sync, session_id)
|
||||
|
||||
|
||||
def _get_session_video_sync(session_id: str):
|
||||
video_rec_map = session_repo.get_video_recordings_map()
|
||||
video_idx = media_service.build_video_index()
|
||||
row = session_repo.get_session_by_id(session_id)
|
||||
|
||||
Reference in new issue
Block a user