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