Files
gitrusprusandCursor 9dfe713668 Add CP→CA→PI platform foundation on MapMil monorepo.
Unify parsing workers, analytics API with PostgreSQL, map UI, and PI distribution into centers/ with Docker Compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 11:43:27 +03:00

196 lines
6.0 KiB
Python

from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from .database import Base, engine, get_db
from .models import MapObject, ObjectMedia
from .schemas import MapObjectCreate, MapObjectRead, MapObjectUpdate, ObjectMediaRead
from .seed import seed_objects
from .storage import (
ALLOWED_CONTENT_TYPES,
MAX_FILE_SIZE,
build_stored_name,
ensure_upload_dir,
is_allowed_content_type,
media_file_path,
remove_media_file,
)
def media_to_read(media: ObjectMedia) -> ObjectMediaRead:
return ObjectMediaRead(
id=media.id,
object_id=media.object_id,
original_name=media.original_name,
content_type=media.content_type,
size=media.size,
created_at=media.created_at,
url=f"/api/media/{media.id}/file",
)
@asynccontextmanager
async def lifespan(_: FastAPI):
ensure_upload_dir()
Base.metadata.create_all(bind=engine)
db = next(get_db())
try:
seed_objects(db)
finally:
db.close()
yield
app = FastAPI(title="MapMil API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/objects", response_model=list[MapObjectRead])
def list_objects(db: Session = Depends(get_db)):
return db.query(MapObject).order_by(MapObject.id).all()
@app.get("/api/objects/{object_id}", response_model=MapObjectRead)
def get_object(object_id: int, db: Session = Depends(get_db)):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
return obj
@app.post("/api/objects", response_model=MapObjectRead, status_code=201)
def create_object(payload: MapObjectCreate, db: Session = Depends(get_db)):
data = payload.model_dump()
created_at = data.pop("created_at", None)
obj = MapObject(**data)
if created_at is not None:
obj.created_at = created_at
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@app.patch("/api/objects/{object_id}", response_model=MapObjectRead)
def update_object(
object_id: int,
payload: MapObjectUpdate,
db: Session = Depends(get_db),
):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(obj, field, value)
db.commit()
db.refresh(obj)
return obj
@app.delete("/api/objects/{object_id}", status_code=204)
def delete_object(object_id: int, db: Session = Depends(get_db)):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
for media in obj.media:
remove_media_file(media.stored_name)
db.delete(obj)
db.commit()
@app.get("/api/objects/{object_id}/media", response_model=list[ObjectMediaRead])
def list_object_media(object_id: int, db: Session = Depends(get_db)):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
media_items = (
db.query(ObjectMedia)
.filter(ObjectMedia.object_id == object_id)
.order_by(ObjectMedia.id)
.all()
)
return [media_to_read(item) for item in media_items]
@app.post("/api/objects/{object_id}/media", response_model=ObjectMediaRead, status_code=201)
async def upload_object_media(
object_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_db),
):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
content_type = file.content_type or "application/octet-stream"
if not is_allowed_content_type(content_type):
raise HTTPException(
status_code=400,
detail=f"Неподдерживаемый тип файла. Разрешены: {', '.join(sorted(ALLOWED_CONTENT_TYPES))}",
)
content = await file.read()
if len(content) > MAX_FILE_SIZE:
raise HTTPException(status_code=400, detail="Файл слишком большой (макс. 20 МБ)")
original_name = file.filename or "file"
stored_name = build_stored_name(original_name)
path = media_file_path(stored_name)
path.write_bytes(content)
media = ObjectMedia(
object_id=object_id,
stored_name=stored_name,
original_name=original_name,
content_type=content_type,
size=len(content),
)
db.add(media)
db.commit()
db.refresh(media)
return media_to_read(media)
@app.get("/api/media/{media_id}/file")
def get_media_file(media_id: int, db: Session = Depends(get_db)):
media = db.query(ObjectMedia).filter(ObjectMedia.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Медиафайл не найден")
path = media_file_path(media.stored_name)
if not path.exists():
raise HTTPException(status_code=404, detail="Файл не найден на диске")
return FileResponse(path, media_type=media.content_type, filename=media.original_name)
@app.delete("/api/media/{media_id}", status_code=204)
def delete_media(media_id: int, db: Session = Depends(get_db)):
media = db.query(ObjectMedia).filter(ObjectMedia.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Медиафайл не найден")
remove_media_file(media.stored_name)
db.delete(media)
db.commit()